summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb629
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb131
-rw-r--r--spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb88
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/bulk_imports/visibility_level_examples.rb37
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb3
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb111
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/abuse_report_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/confidential_notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb415
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/deploy_token_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/explore/sidebar_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/incident_details_routing_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb2
-rw-r--r--spec/support/shared_examples/features/milestone_editing_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb105
-rw-r--r--spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb61
-rw-r--r--spec/support/shared_examples/features/reportable_note_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb202
-rw-r--r--spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/trial_email_validation_shared_example.rb67
-rw-r--r--spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb66
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb265
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/mutation_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb123
-rw-r--r--spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb195
-rw-r--r--spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/helpers/callouts_for_web_hooks.rb49
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb6
-rw-r--r--spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb9
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb131
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb104
-rw-r--r--spec/support/shared_examples/lib/menus_shared_examples.rb55
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb78
-rw-r--r--spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/mailers/export_csv_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/metrics_instrumentation_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/models/active_record_enum_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/models/ci/token_format_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/models/clusters/prometheus_client_shared.rb10
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb114
-rw-r--r--spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb19
-rw-r--r--spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb36
-rw-r--r--spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb106
-rw-r--r--spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb21
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/models/database_event_tracking_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb2
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb21
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb265
-rw-r--r--spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/resource_event_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/observability/csp_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/observability/embed_observabilities_examples.rb61
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb60
-rw-r--r--spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb161
-rw-r--r--spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb20
-rw-r--r--spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb123
-rw-r--r--spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/redis/redis_shared_examples.rb429
-rw-r--r--spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/admin_mode_shared_examples.rb111
-rw-r--r--spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/requests/api/discussions_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/hooks_shared_examples.rb100
-rw-r--r--spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/api/labels_api_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/milestones_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb88
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb448
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/requests/api/snippets_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/requests/applications_controller_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/requests/graphql_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/self_monitoring_shared_examples.rb130
-rw-r--r--spec/support/shared_examples/requests/user_activity_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/security_training_providers_importer.rb4
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/serializers/note_entity_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/services/base_helm_service_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/services/clusters/create_service_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb55
-rw-r--r--spec/support/shared_examples/services/deploy_token_shared_examples.rb88
-rw-r--r--spec/support/shared_examples/services/import_csv_service_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb107
-rw-r--r--spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb137
-rw-r--r--spec/support/shared_examples/services/issuable/update_service_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb141
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/services/service_response_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/views/pipeline_status_changes_email.rb14
-rw-r--r--spec/support/shared_examples/work_items/export_and_import_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb14
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb206
-rw-r--r--spec/support/shared_examples/workers/self_monitoring_shared_examples.rb28
192 files changed, 7386 insertions, 1907 deletions
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
new file mode 100644
index 00000000000..9c096c5a158
--- /dev/null
+++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
@@ -0,0 +1,629 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'validation on Time arguments' do
+ context 'when `to` parameter is higher than `from`' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 1.day.ago.iso8601,
+ to: 2.days.ago.iso8601
+ }
+ end
+
+ it 'returns error' do
+ expect(result).to be_nil
+ expect(graphql_errors.first['message']).to include('`from` argument must be before `to` argument')
+ end
+ end
+
+ context 'when from and to parameter range is higher than 180 days' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: Time.now,
+ to: 181.days.from_now
+ }
+ end
+
+ it 'returns error' do
+ expect(result).to be_nil
+ expect(graphql_errors.first['message']).to include('Max of 180 days timespan is allowed')
+ end
+ end
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics issueCount examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ let_it_be(:issue1) { create(:issue, project: project1, author: author, created_at: 12.days.ago) }
+ let_it_be(:issue2) { create(:issue, project: project2, author: author, created_at: 13.days.ago) }
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ created_at: 14.days.ago)
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ created_at: 15.days.ago)
+ end
+
+ let_it_be(:issue_outside_of_the_range) { create(:issue, project: project2, author: author, created_at: 50.days.ago) }
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ issueCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'issueCount')
+ end
+
+ it 'returns the correct count' do
+ expect(result).to eq({
+ 'identifier' => 'issues',
+ 'unit' => nil,
+ 'value' => 4,
+ 'title' => n_('New Issue', 'New Issues', 4)
+ })
+ end
+
+ context 'with partial filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to eq({
+ 'identifier' => 'issues',
+ 'unit' => nil,
+ 'value' => 2,
+ 'title' => n_('New Issue', 'New Issues', 2)
+ })
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to eq({
+ 'identifier' => 'issues',
+ 'unit' => nil,
+ 'value' => 1,
+ 'title' => n_('New Issue', 'New Issues', 1)
+ })
+ end
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ it_behaves_like 'validation on Time arguments'
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examples' do
+ let_it_be(:deployment1) do
+ create(:deployment, :success, environment: production_environment1, finished_at: 5.days.ago)
+ end
+
+ let_it_be(:deployment2) do
+ create(:deployment, :success, environment: production_environment2, finished_at: 10.days.ago)
+ end
+
+ let_it_be(:deployment3) do
+ create(:deployment, :success, environment: production_environment2, finished_at: 15.days.ago)
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 12.days.ago.iso8601,
+ to: 3.days.ago.iso8601
+ }
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ deploymentCount(from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'deploymentCount')
+ end
+
+ it 'returns the correct count' do
+ expect(result).to eq({
+ 'identifier' => 'deploys',
+ 'unit' => nil,
+ 'value' => 2,
+ 'title' => n_('Deploy', 'Deploys', 2)
+ })
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 20.days.ago.iso8601,
+ to: 18.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to eq({
+ 'identifier' => 'deploys',
+ 'unit' => nil,
+ 'value' => 0,
+ 'title' => n_('Deploy', 'Deploys', 0)
+ })
+ end
+ end
+
+ it_behaves_like 'validation on Time arguments'
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics leadTime examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ let_it_be(:issue1) do
+ create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago)
+ end
+
+ let_it_be(:issue2) do
+ create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago)
+ end
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ created_at: 14.days.ago,
+ closed_at: 11.days.ago)
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ created_at: 20.days.ago,
+ closed_at: 15.days.ago)
+ end
+
+ before do
+ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ leadTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ links {
+ label
+ url
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 21.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'leadTime')
+ end
+
+ it 'returns the correct value' do
+ expect(result).to match(a_hash_including({
+ 'identifier' => 'lead_time',
+ 'unit' => n_('day', 'days', 4),
+ 'value' => 4,
+ 'title' => _('Lead Time'),
+ 'links' => [
+ { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) },
+ { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) }
+ ]
+ }))
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 30.days.ago.iso8601,
+ to: 25.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to match(a_hash_including({ 'value' => nil }))
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to match(a_hash_including({ 'value' => 3 }))
+ end
+ end
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ let_it_be(:issue1) do
+ create(:issue, project: project1, author: author, closed_at: 12.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 17.days.ago)
+ end
+ end
+
+ let_it_be(:issue2) do
+ create(:issue, project: project2, author: author, closed_at: 13.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 16.days.ago)
+ end
+ end
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ closed_at: 11.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 14.days.ago)
+ end
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ closed_at: 15.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 20.days.ago)
+ end
+ end
+
+ before do
+ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ cycleTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ links {
+ label
+ url
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 21.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'cycleTime')
+ end
+
+ it 'returns the correct value' do
+ expect(result).to eq({
+ 'identifier' => 'cycle_time',
+ 'unit' => n_('day', 'days', 4),
+ 'value' => 4,
+ 'title' => _('Cycle Time'),
+ 'links' => []
+ })
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 30.days.ago.iso8601,
+ to: 25.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to match(a_hash_including({ 'value' => nil }))
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to match(a_hash_including({ 'value' => 3 }))
+ end
+ end
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics issuesCompleted examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ # we don't care about opened date, only closed date.
+ let_it_be(:issue1) do
+ create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago)
+ end
+
+ let_it_be(:issue2) do
+ create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago)
+ end
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ created_at: 14.days.ago,
+ closed_at: 11.days.ago)
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ created_at: 20.days.ago,
+ closed_at: 15.days.ago)
+ end
+
+ before do
+ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ issuesCompletedCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ links {
+ label
+ url
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 21.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'issuesCompletedCount')
+ end
+
+ it 'returns the correct value' do
+ expect(result).to match(a_hash_including({
+ 'identifier' => 'issues_completed',
+ 'unit' => n_('issue', 'issues', 4),
+ 'value' => 4,
+ 'title' => _('Issues Completed'),
+ 'links' => [
+ { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) },
+ { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) }
+ ]
+ }))
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 30.days.ago.iso8601,
+ to: 25.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to match(a_hash_including({ 'value' => 0.0 }))
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to match(a_hash_including({ 'value' => 1.0 }))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
new file mode 100644
index 00000000000..ef9830fbce8
--- /dev/null
+++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'unlicensed cycle analytics request params' do
+ let(:params) do
+ {
+ created_after: '2019-01-01',
+ created_before: '2019-03-01',
+ project_ids: [2, 3],
+ namespace: namespace,
+ current_user: user
+ }
+ end
+
+ subject { described_class.new(params) }
+
+ before do
+ root_group.add_owner(user)
+ end
+
+ describe 'validations' do
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+
+ context 'when `created_before` is missing' do
+ before do
+ params[:created_before] = nil
+ end
+
+ it 'is valid', time_travel_to: '2019-03-01' do
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'when `created_before` is earlier than `created_after`' do
+ before do
+ params[:created_before] = '2015-01-01'
+ end
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:created_before]).not_to be_empty
+ end
+ end
+
+ context 'when the date range exceeds 180 days' do
+ before do
+ params[:created_before] = '2019-07-15'
+ end
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ message = s_('CycleAnalytics|The given date range is larger than 180 days')
+ expect(subject.errors.messages[:created_after]).to include(message)
+ end
+ end
+ end
+
+ it 'casts `created_after` to `Time`' do
+ expect(subject.created_after).to be_a_kind_of(Time)
+ end
+
+ it 'casts `created_before` to `Time`' do
+ expect(subject.created_before).to be_a_kind_of(Time)
+ end
+
+ describe 'optional `value_stream`' do
+ context 'when `value_stream` is not empty' do
+ let(:value_stream) { instance_double('Analytics::CycleAnalytics::ValueStream') }
+
+ before do
+ params[:value_stream] = value_stream
+ end
+
+ it { expect(subject.value_stream).to eq(value_stream) }
+ end
+
+ context 'when `value_stream` is nil' do
+ before do
+ params[:value_stream] = nil
+ end
+
+ it { expect(subject.value_stream).to eq(nil) }
+ end
+ end
+
+ describe 'sorting params' do
+ before do
+ params.merge!(sort: 'duration', direction: 'asc')
+ end
+
+ it 'converts sorting params to symbol when passing it to data collector' do
+ data_collector_params = subject.to_data_collector_params
+
+ expect(data_collector_params[:sort]).to eq(:duration)
+ expect(data_collector_params[:direction]).to eq(:asc)
+ end
+
+ it 'adds sorting params to data attributes' do
+ data_attributes = subject.to_data_attributes
+
+ expect(data_attributes[:sort]).to eq('duration')
+ expect(data_attributes[:direction]).to eq('asc')
+ end
+ end
+
+ describe 'aggregation params' do
+ context 'when not licensed' do
+ it 'returns nil' do
+ data_collector_params = subject.to_data_attributes
+ expect(data_collector_params[:aggregation]).to eq(nil)
+ end
+ end
+ end
+
+ describe 'use_aggregated_data_collector param' do
+ subject(:value) { described_class.new(params).to_data_collector_params[:use_aggregated_data_collector] }
+
+ it { is_expected.to eq(false) }
+ end
+
+ describe 'feature availablity data attributes' do
+ subject(:value) { described_class.new(params).to_data_attributes }
+
+ it 'disables all paid features' do
+ is_expected.to match(a_hash_including(enable_tasks_by_type_chart: 'false',
+ enable_customizable_stages: 'false',
+ enable_projects_filter: 'false'))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb b/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb
new file mode 100644
index 00000000000..618be53cb3b
--- /dev/null
+++ b/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+# These shared_examples require the following variables:
+# - text: The text to be run through the filter
+#
+# Usage:
+#
+# it_behaves_like 'html filter timeout' do
+# let(:text) { 'some text' }
+# end
+RSpec.shared_examples 'html filter timeout' do
+ context 'when rendering takes too long' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:context) { { project: project } }
+
+ it 'times out' do
+ stub_const("Banzai::Filter::TimeoutHtmlPipelineFilter::RENDER_TIMEOUT", 0.1)
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:call_with_timeout) do
+ sleep(0.2)
+ text
+ end
+ end
+
+ expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Timeout::Error),
+ project_id: context[:project].id,
+ class_name: described_class.name.demodulize
+ )
+
+ result = filter(text)
+
+ expect(result.to_html).to eq text
+ end
+ end
+end
+
+# Usage:
+#
+# it_behaves_like 'text html filter timeout' do
+# let(:text) { 'some text' }
+# end
+RSpec.shared_examples 'text filter timeout' do
+ context 'when rendering takes too long' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:context) { { project: project } }
+
+ it 'times out' do
+ stub_const("Banzai::Filter::TimeoutTextPipelineFilter::RENDER_TIMEOUT", 0.1)
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:call_with_timeout) do
+ sleep(0.2)
+ text
+ end
+ end
+
+ expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Timeout::Error),
+ project_id: context[:project].id,
+ class_name: described_class.name.demodulize
+ )
+
+ result = filter(text)
+
+ expect(result).to eq text
+ end
+ end
+end
diff --git a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
index 599161abbfe..8f2f3f89914 100644
--- a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
+++ b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
@@ -7,6 +7,10 @@ RSpec.shared_examples 'a metrics embed filter' do
let(:input) { %(<a href="#{url}">example</a>) }
let(:doc) { filter(input) }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
context 'when the document has an external link' do
let(:url) { 'https://foo.com' }
@@ -38,6 +42,18 @@ RSpec.shared_examples 'a metrics embed filter' do
expect(doc.at_css('.js-render-metrics')).to be_present
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'does not append a metrics chart placeholder' do
+ node = doc.at_css('.js-render-metrics')
+
+ expect(node).not_to be_present
+ end
+ end
end
# Nokogiri escapes the URLs, but we don't care about that
diff --git a/spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb b/spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb
new file mode 100644
index 00000000000..6912bcaee34
--- /dev/null
+++ b/spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# Specs for reference links containing HTML.
+#
+# Requires a reference:
+# let(:reference) { '#42' }
+RSpec.shared_examples 'a reference containing an element node' do
+ let(:inner_html) { 'element <code>node</code> inside' }
+ let(:reference_with_element) { %(<a href="#{reference}">#{inner_html}</a>) }
+
+ it 'does not escape inner html' do
+ doc = reference_filter(reference_with_element)
+ expect(doc.children.first.inner_html).to eq(inner_html)
+ end
+end
+
+# Requires a reference, subject and subject_name:
+# subject { create(:user) }
+# let(:reference) { subject.to_reference }
+# let(:subject_name) { 'user' }
+RSpec.shared_examples 'user reference or project reference' do
+ shared_examples 'it contains a data- attribute' do
+ it 'includes a data- attribute' do
+ doc = reference_filter("Hey #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute("data-#{subject_name}")
+ expect(link.attr("data-#{subject_name}")).to eq subject.id.to_s
+ end
+ end
+
+ context 'when mentioning a resource' do
+ it_behaves_like 'a reference containing an element node'
+ it_behaves_like 'it contains a data- attribute'
+
+ it "links to a resource" do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.send("#{subject_name}_url", subject)
+ end
+
+ it 'links to a resource with a period' do
+ subject = create(subject_name.to_sym, name: 'alphA.Beta')
+
+ doc = reference_filter("Hey #{get_reference(subject)}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'links to a resource with an underscore' do
+ subject = create(subject_name.to_sym, name: 'ping_pong_king')
+
+ doc = reference_filter("Hey #{get_reference(subject)}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'links to a resource with different case-sensitivity' do
+ subject = create(subject_name.to_sym, name: 'RescueRanger')
+ reference = get_reference(subject)
+
+ doc = reference_filter("Hey #{reference.upcase}")
+ expect(doc.css('a').length).to eq 1
+ expect(doc.css('a').text).to eq(reference)
+ end
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Hey #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r{https?://}
+ expect(link).to eq urls.send "#{subject_name}_path", subject
+ end
+
+ describe 'referencing a resource in a link href' do
+ let(:reference) { %(<a href="#{get_reference(subject)}">Some text</a>) }
+
+ it_behaves_like 'it contains a data- attribute'
+
+ it 'links to the resource' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.send "#{subject_name}_url", subject
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Mention me (#{reference}.)")
+ expect(doc.to_html).to match(%r{\(<a.+>Some text</a>\.\)})
+ end
+ end
+end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 7df4b7635d3..ddd3bbd636a 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -80,7 +80,7 @@ RSpec.shared_examples 'multiple issue boards' do
click_button 'Select a label'
- page.choose(planning.title)
+ find('label', text: planning.title).click
click_button 'Add to board'
diff --git a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
index 40e9726f89c..02eae250e6a 100644
--- a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
+++ b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
@@ -27,14 +27,6 @@ RSpec.shared_examples 'visibility level settings' do
expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
-
- context 'when destination is blank' do
- let(:destination_namespace) { '' }
-
- it 'sets visibility level to public' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
- end
end
context 'when internal' do
@@ -63,27 +55,6 @@ RSpec.shared_examples 'visibility level settings' do
expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
-
- context 'when destination is blank' do
- let(:destination_namespace) { '' }
-
- it 'sets visibility level to internal' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
-
- context 'when visibility level is restricted' do
- it 'sets visibility level to private' do
- stub_application_setting(
- restricted_visibility_levels: [
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC
- ]
- )
-
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
- end
- end
end
context 'when private' do
@@ -112,13 +83,5 @@ RSpec.shared_examples 'visibility level settings' do
expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
-
- context 'when destination is blank' do
- let(:destination_namespace) { '' }
-
- it 'sets visibility level to private' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
- end
end
end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index de38d1ff9f8..af1843bae28 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -138,6 +138,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
.not_to exceed_all_query_limit(control_count)
end
+ context 'when user is not allowed to import projects' do
+ let(:user) { create(:user) }
+ let!(:group) { create(:group).tap { |group| group.add_developer(user) } }
+
+ it 'returns 404' do
+ expect(stub_client(repos: [], orgs: [])).to receive(:repos)
+
+ get :status, params: { namespace_id: group.id }, format: :html
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'when filtering' do
let(:repo_2) { repo_fake.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) }
let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') }
diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
index 44baadaaade..e94f063399d 100644
--- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
@@ -19,4 +19,26 @@ RSpec.shared_examples 'import controller status' do
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
end
+
+ context 'when format is html' do
+ context 'when namespace_id is present' do
+ let!(:developer_group) { create(:group).tap { |g| g.add_developer(user) } }
+
+ context 'when user cannot import projects' do
+ it 'returns 404' do
+ get :status, params: { namespace_id: developer_group.id }, format: :html
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user can import projects' do
+ it 'returns 200' do
+ get :status, params: { namespace_id: group.id }, format: :html
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb b/spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb
new file mode 100644
index 00000000000..66d753a4010
--- /dev/null
+++ b/spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'project import rate limiter' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when limit exceeds' do
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+ end
+
+ it 'notifies and redirects user' do
+ post :create, params: {}
+
+ expect(flash[:alert]).to eq('This endpoint has been requested too many times. Try again later.')
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
index cc28a79b4ca..e75188f8249 100644
--- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
@@ -60,19 +60,6 @@ RSpec.shared_examples Repositories::GitHttpController do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'updates the user activity' do
- activity_project = container.is_a?(PersonalSnippet) ? nil : project
-
- activity_service = instance_double(Users::ActivityService)
-
- args = { author: user, project: activity_project, namespace: activity_project&.namespace }
- expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service)
-
- expect(activity_service).to receive(:execute)
-
- get :info_refs, params: params
- end
-
include_context 'parsed logs' do
it 'adds user info to the logs' do
get :info_refs, params: params
@@ -87,14 +74,20 @@ RSpec.shared_examples Repositories::GitHttpController do
end
describe 'POST #git_upload_pack' do
- before do
+ it 'returns 200' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
- end
- it 'returns 200' do
post :git_upload_pack, params: params
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when JWT token is not provided' do
+ it 'returns 403' do
+ post :git_upload_pack, params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb
index 112b9cbb204..f658cfac0f5 100644
--- a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb
@@ -15,8 +15,9 @@ RSpec.shared_examples 'snippets sort order' do
context 'when no sort param is provided' do
it 'calls SnippetsFinder with updated_at sort option' do
- expect(SnippetsFinder).to receive(:new).with(user,
- hash_including(sort: 'updated_desc')).and_call_original
+ expect(SnippetsFinder).to receive(:new)
+ .with(user, hash_including(sort: 'updated_desc'))
+ .and_call_original
subject
end
@@ -27,8 +28,9 @@ RSpec.shared_examples 'snippets sort order' do
let(:sort_argument) { { sort: order } }
it 'calls SnippetsFinder with the given sort param' do
- expect(SnippetsFinder).to receive(:new).with(user,
- hash_including(sort: order)).and_call_original
+ expect(SnippetsFinder).to receive(:new)
+ .with(user, hash_including(sort: order))
+ .and_call_original
subject
end
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
index 38c3157e898..b5528afa0b5 100644
--- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -7,6 +7,9 @@
RSpec.shared_examples 'tracking unique hll events' do
it 'tracks unique event' do
+ # Allow any event tracking before we expect the specific event we want to check below
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).and_call_original
+
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(
receive(:track_event)
.with(target_event, values: expected_value)
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index 5d77ed5fdfc..32aa566c27e 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -15,20 +15,33 @@ RSpec.shared_examples 'wiki controller actions' do
sign_in(user)
end
- shared_examples 'recovers from git timeout' do
+ shared_examples 'recovers from git errors' do
let(:method_name) { :page }
- context 'when we encounter git command errors' do
+ context 'when we encounter CommandTimedOut error' do
it 'renders the appropriate template', :aggregate_failures do
- expect(controller).to receive(method_name) do
- raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded'
- end
+ expect(controller)
+ .to receive(method_name)
+ .and_raise(::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded')
request
expect(response).to render_template('shared/wikis/git_error')
end
end
+
+ context 'when we encounter a NoRepository error' do
+ it 'renders the appropriate template', :aggregate_failures do
+ expect(controller)
+ .to receive(method_name)
+ .and_raise(Gitlab::Git::Repository::NoRepository)
+
+ request
+
+ expect(response).to render_template('shared/wikis/empty')
+ expect(assigns(:error)).to eq('Could not access the Wiki Repository at this time.')
+ end
+ end
end
describe 'GET #new' do
@@ -65,7 +78,7 @@ RSpec.shared_examples 'wiki controller actions' do
get :pages, params: routing_params.merge(id: wiki_title)
end
- it_behaves_like 'recovers from git timeout' do
+ it_behaves_like 'recovers from git errors' do
subject(:request) { get :pages, params: routing_params.merge(id: wiki_title) }
let(:method_name) { :wiki_pages }
@@ -122,7 +135,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
end
- it_behaves_like 'recovers from git timeout' do
+ it_behaves_like 'recovers from git errors' do
subject(:request) { get :history, params: routing_params.merge(id: wiki_title) }
let(:allow_read_wiki) { true }
@@ -170,7 +183,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
end
- it_behaves_like 'recovers from git timeout' do
+ it_behaves_like 'recovers from git errors' do
subject(:request) { get :diff, params: routing_params.merge(id: wiki_title, version_id: wiki.repository.commit.id) }
end
end
@@ -185,7 +198,7 @@ RSpec.shared_examples 'wiki controller actions' do
context 'when page exists' do
let(:id) { wiki_title }
- it_behaves_like 'recovers from git timeout'
+ it_behaves_like 'recovers from git errors'
it 'renders the page' do
request
@@ -366,7 +379,7 @@ RSpec.shared_examples 'wiki controller actions' do
subject(:request) { get(:edit, params: routing_params.merge(id: id_param)) }
it_behaves_like 'edit action'
- it_behaves_like 'recovers from git timeout'
+ it_behaves_like 'recovers from git errors'
context 'when page content encoding is valid' do
render_views
@@ -386,11 +399,10 @@ RSpec.shared_examples 'wiki controller actions' do
let(:id_param) { wiki_title }
subject(:request) do
- patch(:update,
- params: routing_params.merge(
- id: id_param,
- wiki: { title: new_title, content: new_content }
- ))
+ patch(:update, params: routing_params.merge(
+ id: id_param,
+ wiki: { title: new_title, content: new_content }
+ ))
end
it_behaves_like 'edit action'
@@ -426,10 +438,9 @@ RSpec.shared_examples 'wiki controller actions' do
let(:new_content) { 'New content' }
subject(:request) do
- post(:create,
- params: routing_params.merge(
- wiki: { title: new_title, content: new_content }
- ))
+ post(:create, params: routing_params.merge(
+ wiki: { title: new_title, content: new_content }
+ ))
end
context 'when page is valid' do
@@ -463,10 +474,9 @@ RSpec.shared_examples 'wiki controller actions' do
let(:delete_user) { user }
subject(:request) do
- delete(:destroy,
- params: routing_params.merge(
- id: id_param
- ))
+ delete(:destroy, params: routing_params.merge(
+ id: id_param
+ ))
end
before do
diff --git a/spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb b/spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb
new file mode 100644
index 00000000000..4e8d65ac25e
--- /dev/null
+++ b/spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'raises an error when specifying an invalid factory' do
+ it 'raises an error' do
+ expect { parser.parse }.to raise_error(RuntimeError, /invalids.*to a valid registered Factory/)
+ end
+end
+
+RSpec.shared_examples 'specifying invalid traits to a factory' do
+ it 'raises an error', :aggregate_failures do
+ expect { parser.parse }.to raise_error do |error|
+ expect(error).to be_a(RuntimeError)
+ expect(error.message).to include('Trait not registered: \\"invalid\\"')
+ expect(error.message).to include('for Factory \\"issue\\"')
+ end
+ end
+end
+
+RSpec.shared_examples 'specifying invalid attributes to a factory' do
+ it 'raises an error' do
+ expect { parser.parse }.to raise_error(RuntimeError, /is not a valid attribute/)
+ end
+
+ it 'contains possible alternatives' do
+ expect { parser.parse }.to raise_error(RuntimeError, /Did you mean/)
+ end
+end
+
+RSpec.shared_examples 'an id already exists' do
+ it 'raises a validation error' do
+ expect { parser.parse }.to raise_error(/id `my_label` must be unique/)
+ end
+end
+
+RSpec.shared_examples 'name is not specified' do
+ it 'raises an error when name is not specified' do
+ expect { parser.parse }.to raise_error(/Seed file must specify a name/)
+ end
+end
+
+RSpec.shared_examples 'factory definitions' do
+ it 'has exactly two definitions' do
+ parser.parse
+
+ expect(parser.definitions.size).to eq(2)
+ end
+
+ it 'creates the group label' do
+ expect { parser.parse }.to change { GroupLabel.count }.by(1)
+ end
+
+ it 'creates the project' do
+ expect { parser.parse }.to change { Project.count }.by(1)
+ end
+end
+
+RSpec.shared_examples 'passes traits' do
+ it 'passes traits' do
+ expect_next_instance_of(Gitlab::DataSeeder::FactoryDefinitions::FactoryDefinition) do |instance|
+ # `described` trait will automaticaly generate a description
+ expect(instance.build(binding).description).to eq('Description of Test Label')
+ end
+
+ parser.parse
+ end
+end
+
+RSpec.shared_examples 'has a name' do
+ it 'has a name' do
+ parser.parse
+
+ expect(parser.name).to eq('Test')
+ end
+end
+
+RSpec.shared_examples 'definition has an id' do
+ it 'binds the object', :aggregate_failures do
+ parser.parse
+
+ expect(group_labels).to be_a(OpenStruct) # rubocop:disable Style/OpenStructUse
+ expect(group_labels.my_label).to be_a(GroupLabel)
+ expect(group_labels.my_label.title).to eq('My Label')
+ end
+end
+
+RSpec.shared_examples 'id has spaces' do
+ it 'binds to an underscored variable', :aggregate_failures do
+ parser.parse
+
+ expect(group_labels).to respond_to(:id_with_spaces)
+ expect(group_labels.id_with_spaces.title).to eq('With Spaces')
+ end
+
+ it 'renders a warning' do
+ expect { parser.parse }.to output(%(parsing id "id with spaces" as "id_with_spaces"\n)).to_stderr
+ end
+end
+
+RSpec.shared_examples 'definition does not have an id' do
+ it 'does not bind the object' do
+ parser.parse
+
+ expect(group_labels.to_h).to be_empty
+ end
+end
+
+RSpec.shared_examples 'invalid id' do |message|
+ it 'raises an error' do
+ expect { parser.parse }.to raise_error(message)
+ end
+end
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index 44f30c32472..6c4e98c9989 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
RSpec.shared_examples 'hardware device for 2fa' do |device_type|
- include Spec::Support::Helpers::Features::TwoFactorHelpers
+ include Features::TwoFactorHelpers
include Spec::Support::Helpers::ModalHelpers
def register_device(device_type, **kwargs)
case device_type.downcase
- when "u2f"
- register_u2f_device(**kwargs)
when "webauthn"
register_webauthn_device(**kwargs)
else
@@ -98,9 +96,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
end
it 'provides a button that shows the fallback otp code UI' do
- expect(page).to have_link('Sign in via 2FA code')
-
- click_link('Sign in via 2FA code')
+ click_button(_('Sign in via 2FA code'))
assert_fallback_ui(page)
end
diff --git a/spec/support/shared_examples/features/abuse_report_shared_examples.rb b/spec/support/shared_examples/features/abuse_report_shared_examples.rb
new file mode 100644
index 00000000000..ea9b4e9f4b2
--- /dev/null
+++ b/spec/support/shared_examples/features/abuse_report_shared_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'reports the user with an abuse category' do
+ it 'creates abuse report' do
+ click_button 'Report abuse'
+ choose "They're posting spam."
+ click_button 'Next'
+
+ page.attach_file('spec/fixtures/dk.png') do
+ click_button "Choose file"
+ end
+
+ fill_in 'abuse_report_message', with: 'This user sends spam'
+ click_button 'Send report'
+
+ expect(page).to have_content 'Thank you for your report'
+ end
+end
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index 32a7b32ac72..3c78869ffaa 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples 'resource access tokens missing access rights' do
end
RSpec.shared_examples 'resource access tokens creation' do |resource_type|
- include Spec::Support::Helpers::AccessTokenHelpers
+ include Features::AccessTokenHelpers
it 'allows creation of an access token', :aggregate_failures do
name = 'My access token'
diff --git a/spec/support/shared_examples/features/confidential_notes_shared_examples.rb b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb
index 289da025af6..cd0e8f94934 100644
--- a/spec/support/shared_examples/features/confidential_notes_shared_examples.rb
+++ b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.shared_examples 'confidential notes on issuables' do
- include Spec::Support::Helpers::Features::NotesHelpers
+ include Features::NotesHelpers
context 'when user does not have permissions' do
it 'does not show confidential note checkbox' do
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 6cd9c4ce1c4..41114197ff5 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -1,116 +1,310 @@
# frozen_string_literal: true
+require 'spec_helper'
+
RSpec.shared_examples 'edits content using the content editor' do
+ include ContentEditorHelpers
+
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
- def switch_to_content_editor
- click_button _('View rich text')
- click_button _('Rich text')
- end
+ let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
+ let(:modifier_key) { is_mac ? :command : :control }
- def type_in_content_editor(keys)
- find(content_editor_testid).send_keys keys
- end
+ it 'saves page content in local storage if the user navigates away' do
+ switch_to_content_editor
- def open_insert_media_dropdown
- page.find('svg[data-testid="media-icon"]').click
- end
+ expect(page).to have_css(content_editor_testid)
- def set_source_editor_content(content)
- find('.js-gfm-input').set content
- end
+ type_in_content_editor ' Typing text in the content editor'
- def expect_formatting_menu_to_be_visible
- expect(page).to have_css('[data-testid="formatting-bubble-menu"]')
- end
+ wait_until_hidden_field_is_updated /Typing text in the content editor/
- def expect_formatting_menu_to_be_hidden
- expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]')
- end
+ begin
+ refresh
+ rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError
+ end
- def expect_media_bubble_menu_to_be_visible
- expect(page).to have_css('[data-testid="media-bubble-menu"]')
+ expect(page).to have_text('Typing text in the content editor')
end
- def upload_asset(fixture_name)
- attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true)
- end
+ describe 'creating and editing links' do
+ before do
+ switch_to_content_editor
+ end
- def wait_until_hidden_field_is_updated(value)
- expect(page).to have_field('wiki[content]', with: value, type: 'hidden')
- end
+ context 'when clicking the link icon in the toolbar' do
+ it 'shows the link bubble menu' do
+ page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
- it 'saves page content in local storage if the user navigates away' do
- switch_to_content_editor
+ expect(page).to have_css('[data-testid="link-bubble-menu"]')
+ end
- expect(page).to have_css(content_editor_testid)
+ context 'if no text is selected' do
+ before do
+ page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
+ end
+
+ it 'opens an empty inline modal to create a link' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ expect(page).to have_field('link-text', with: '')
+ expect(page).to have_field('link-href', with: '')
+ end
+ end
+
+ context 'when the user clicks the apply button' do
+ it 'applies the changes to the document' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ fill_in 'link-text', with: 'Link to GitLab home page'
+ fill_in 'link-href', with: 'https://gitlab.com'
+
+ click_button 'Apply'
+ end
+
+ page.within content_editor_testid do
+ expect(page).to have_css('a[href="https://gitlab.com"]')
+ expect(page).to have_text('Link to GitLab home page')
+ end
+ end
+ end
+
+ context 'when the user clicks the cancel button' do
+ it 'does not apply the changes to the document' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ fill_in 'link-text', with: 'Link to GitLab home page'
+ fill_in 'link-href', with: 'https://gitlab.com'
+
+ click_button 'Cancel'
+ end
+
+ page.within content_editor_testid do
+ expect(page).not_to have_css('a')
+ end
+ end
+ end
+ end
- type_in_content_editor ' Typing text in the content editor'
+ context 'if text is selected' do
+ before do
+ type_in_content_editor 'The quick brown fox jumps over the lazy dog'
+ type_in_content_editor [:shift, :left]
+ type_in_content_editor [:shift, :left]
+ type_in_content_editor [:shift, :left]
+
+ page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
+ end
+
+ it 'prefills inline modal to create a link' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ expect(page).to have_field('link-text', with: 'dog')
+ expect(page).to have_field('link-href', with: '')
+ end
+ end
+
+ context 'when the user clicks the apply button' do
+ it 'applies the changes to the document' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ fill_in 'link-text', with: 'new dog'
+ fill_in 'link-href', with: 'https://en.wikipedia.org/wiki/Shiba_Inu'
+
+ click_button 'Apply'
+ end
+
+ page.within content_editor_testid do
+ expect(page).to have_selector('a[href="https://en.wikipedia.org/wiki/Shiba_Inu"]',
+ text: 'new dog'
+ )
+ end
+ end
+ end
+ end
+ end
- wait_until_hidden_field_is_updated /Typing text in the content editor/
+ context 'if cursor is placed on an existing link' do
+ before do
+ type_in_content_editor 'Link to [GitLab home **page**](https://gitlab.com)'
+ type_in_content_editor :left
+ end
- refresh
+ it 'prefills inline modal to edit the link' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ page.find('[data-testid="edit-link"]').click
- expect(page).to have_text('Typing text in the content editor')
+ expect(page).to have_field('link-text', with: 'GitLab home page')
+ expect(page).to have_field('link-href', with: 'https://gitlab.com')
+ end
+ end
- refresh # also retained after second refresh
+ it 'updates the link attributes if text is not updated' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ page.find('[data-testid="edit-link"]').click
- expect(page).to have_text('Typing text in the content editor')
+ fill_in 'link-href', with: 'https://about.gitlab.com'
- click_link 'Cancel' # draft is deleted on cancel
+ click_button 'Apply'
+ end
- page.go_back
+ page.within content_editor_testid do
+ expect(page).to have_selector('a[href="https://about.gitlab.com"]')
+ expect(page.find('a')).to have_text('GitLab home page')
+ expect(page).to have_selector('strong', text: 'page')
+ end
+ end
- expect(page).not_to have_text('Typing text in the content editor')
- end
+ it 'updates the link attributes and text if text is updated' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ page.find('[data-testid="edit-link"]').click
- describe 'formatting bubble menu' do
- it 'shows a formatting bubble menu for a regular paragraph and headings' do
- switch_to_content_editor
+ fill_in 'link-text', with: 'GitLab about page'
+ fill_in 'link-href', with: 'https://about.gitlab.com'
- expect(page).to have_css(content_editor_testid)
+ click_button 'Apply'
+ end
- type_in_content_editor 'Typing text in the content editor'
- type_in_content_editor [:shift, :left]
+ page.within content_editor_testid do
+ expect(page).to have_selector('a[href="https://about.gitlab.com"]',
+ text: 'GitLab about page'
+ )
+ expect(page).not_to have_selector('strong')
+ end
+ end
- expect_formatting_menu_to_be_visible
+ it 'does nothing if Cancel is clicked' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ page.find('[data-testid="edit-link"]').click
- type_in_content_editor [:right, :right, :enter, '## Heading']
+ click_button 'Cancel'
+ end
- expect_formatting_menu_to_be_hidden
+ page.within content_editor_testid do
+ expect(page).to have_selector('a[href="https://gitlab.com"]',
+ text: 'GitLab home page'
+ )
+ expect(page).to have_selector('strong')
+ end
+ end
- type_in_content_editor [:shift, :left]
+ context 'when the user clicks the unlink button' do
+ it 'removes the link' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ page.find('[data-testid="remove-link"]').click
+ end
+
+ page.within content_editor_testid do
+ expect(page).not_to have_selector('a')
+ expect(page).to have_selector('strong', text: 'page')
+ end
+ end
+ end
+ end
+
+ context 'when selection spans more than a link' do
+ before do
+ type_in_content_editor 'a [b **c**](https://gitlab.com)'
+
+ type_in_content_editor [:shift, :left]
+ type_in_content_editor [:shift, :left]
+ type_in_content_editor [:shift, :left]
+ type_in_content_editor [:shift, :left]
+ type_in_content_editor [:shift, :left]
+
+ page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
+ end
+
+ it 'prefills inline modal with the entire selection' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ expect(page).to have_field('link-text', with: 'a b c')
+ expect(page).to have_field('link-href', with: '')
+ end
+ end
- expect_formatting_menu_to_be_visible
+ it 'expands the link and updates the link attributes if text is not updated' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ fill_in 'link-href', with: 'https://about.gitlab.com'
+
+ click_button 'Apply'
+ end
+
+ page.within content_editor_testid do
+ expect(page).to have_selector('a[href="https://about.gitlab.com"]')
+ expect(page.find('a')).to have_text('a b c')
+ expect(page).to have_selector('strong', text: 'c')
+ end
+ end
+
+ it 'expands the link, updates the link attributes and text if text is updated' do
+ page.within '[data-testid="link-bubble-menu"]' do
+ fill_in 'link-text', with: 'new text'
+ fill_in 'link-href', with: 'https://about.gitlab.com'
+
+ click_button 'Apply'
+ end
+
+ page.within content_editor_testid do
+ expect(page).to have_selector('a[href="https://about.gitlab.com"]',
+ text: 'new text'
+ )
+ expect(page).not_to have_selector('strong')
+ end
+ end
end
end
- describe 'media elements bubble menu' do
+ describe 'selecting text' do
before do
switch_to_content_editor
- open_insert_media_dropdown
+ # delete all text first
+ type_in_content_editor [modifier_key, 'a']
+ type_in_content_editor :backspace
+
+ type_in_content_editor 'The quick **brown** fox _jumps_ over the lazy dog!'
+ type_in_content_editor :enter
+ type_in_content_editor '[Link](https://gitlab.com)'
+ type_in_content_editor :enter
+ type_in_content_editor 'Jackdaws love my ~~big~~ sphinx of quartz!'
+
+ # select all text
+ type_in_content_editor [modifier_key, 'a']
end
- def test_displays_media_bubble_menu(media_element_selector, fixture_file)
- upload_asset fixture_file
+ it 'renders selected text in a .content-editor-selection class' do
+ page.within content_editor_testid do
+ assert_selected 'The quick'
+ assert_selected 'brown'
+ assert_selected 'fox'
+ assert_selected 'jumps'
+ assert_selected 'over the lazy dog!'
- wait_for_requests
+ assert_selected 'Link'
- expect(page).to have_css(media_element_selector)
+ assert_selected 'Jackdaws love my'
+ assert_selected 'big'
+ assert_selected 'sphinx of quartz!'
+ end
+ end
- page.find(media_element_selector).click
+ def assert_selected(text)
+ expect(page).to have_selector('.content-editor-selection', text: text)
+ end
+ end
- expect_formatting_menu_to_be_hidden
- expect_media_bubble_menu_to_be_visible
+ describe 'media elements bubble menu' do
+ before do
+ switch_to_content_editor
+
+ click_attachment_button
end
it 'displays correct media bubble menu for images', :js do
- test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
+ display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
+
+ expect_media_bubble_menu_to_be_visible
end
it 'displays correct media bubble menu for video', :js do
- test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
+ display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
+
+ expect_media_bubble_menu_to_be_visible
end
end
@@ -150,7 +344,6 @@ RSpec.shared_examples 'edits content using the content editor' do
type_in_content_editor 'var a = 0'
type_in_content_editor [:shift, :left]
- expect_formatting_menu_to_be_hidden
expect(page).to have_css('[data-testid="code-block-bubble-menu"]')
end
@@ -187,8 +380,8 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(iframe['src']).to include('/-/sandbox/mermaid')
within_frame(iframe) do
- expect(find('svg').text).to include('JohnDoe12')
- expect(find('svg').text).to include('HelloWorld34')
+ expect(find('svg .nodes').text).to include('JohnDoe12')
+ expect(find('svg .nodes').text).to include('HelloWorld34')
end
expect(iframe['height'].to_i).to be > 100
@@ -198,12 +391,13 @@ RSpec.shared_examples 'edits content using the content editor' do
within_frame(iframe) do
page.has_content?('JaneDoe34')
- expect(find('svg').text).to include('JaneDoe34')
- expect(find('svg').text).to include('HelloWorld56')
+ expect(find('svg .nodes').text).to include('JaneDoe34')
+ expect(find('svg .nodes').text).to include('HelloWorld56')
end
end
- it 'toggles the diagram when preview button is clicked' do
+ it 'toggles the diagram when preview button is clicked',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397682' do
find('[data-testid="preview-diagram"]').click
expect(find(content_editor_testid)).not_to have_selector('iframe')
@@ -213,8 +407,61 @@ RSpec.shared_examples 'edits content using the content editor' do
iframe = find(content_editor_testid).find('iframe')
within_frame(iframe) do
- expect(find('svg').text).to include('JohnDoe12')
- expect(find('svg').text).to include('HelloWorld34')
+ expect(find('svg .nodes').text).to include('JohnDoe12')
+ expect(find('svg .nodes').text).to include('HelloWorld34')
+ end
+ end
+ end
+
+ describe 'pasting text' do
+ before do
+ switch_to_content_editor
+
+ type_in_content_editor "Some **rich** _text_ ~~content~~ [link](https://gitlab.com)"
+
+ type_in_content_editor [modifier_key, 'a']
+ type_in_content_editor [modifier_key, 'x']
+ end
+
+ it 'pastes text with formatting if ctrl + v is pressed' do
+ type_in_content_editor [modifier_key, 'v']
+
+ page.within content_editor_testid do
+ expect(page).to have_selector('strong', text: 'rich')
+ expect(page).to have_selector('em', text: 'text')
+ expect(page).to have_selector('s', text: 'content')
+ expect(page).to have_selector('a[href="https://gitlab.com"]', text: 'link')
+ end
+ end
+
+ it 'pastes raw text without formatting if shift + ctrl + v is pressed' do
+ type_in_content_editor [modifier_key, :shift, 'v']
+
+ page.within content_editor_testid do
+ expect(page).to have_text('Some rich text content link')
+
+ expect(page).not_to have_selector('strong')
+ expect(page).not_to have_selector('em')
+ expect(page).not_to have_selector('s')
+ expect(page).not_to have_selector('a')
+ end
+ end
+
+ it 'pastes raw text without formatting, stripping whitespaces, if shift + ctrl + v is pressed' do
+ type_in_content_editor " Some **rich**"
+ type_in_content_editor :enter
+ type_in_content_editor " _text_"
+ type_in_content_editor :enter
+ type_in_content_editor " ~~content~~"
+ type_in_content_editor :enter
+ type_in_content_editor " [link](https://gitlab.com)"
+
+ type_in_content_editor [modifier_key, 'a']
+ type_in_content_editor [modifier_key, 'x']
+ type_in_content_editor [modifier_key, :shift, 'v']
+
+ page.within content_editor_testid do
+ expect(page).to have_text('Some rich text content link')
end
end
end
@@ -225,7 +472,7 @@ RSpec.shared_examples 'edits content using the content editor' do
before do
if defined?(project)
create(:issue, project: project, title: 'My Cool Linked Issue')
- create(:merge_request, source_project: project, title: 'My Cool Merge Request')
+ create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:label, project: project, title: 'My Cool Label')
create(:milestone, project: project, title: 'My Cool Milestone')
@@ -234,7 +481,7 @@ RSpec.shared_examples 'edits content using the content editor' do
project = create(:project, group: group)
create(:issue, project: project, title: 'My Cool Linked Issue')
- create(:merge_request, source_project: project, title: 'My Cool Merge Request')
+ create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:group_label, group: group, title: 'My Cool Label')
create(:milestone, group: group, title: 'My Cool Milestone')
@@ -251,7 +498,9 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(find(suggestions_dropdown)).to have_text('abc123')
expect(find(suggestions_dropdown)).to have_text('all')
- expect(find(suggestions_dropdown)).to have_text('Group Members (2)')
+ expect(find(suggestions_dropdown)).to have_text('Group Members')
+
+ type_in_content_editor 'bc'
send_keys [:arrow_down, :enter]
@@ -332,3 +581,23 @@ RSpec.shared_examples 'edits content using the content editor' do
end
end
end
+
+RSpec.shared_examples 'inserts diagrams.net diagram using the content editor' do
+ include ContentEditorHelpers
+
+ before do
+ switch_to_content_editor
+
+ click_attachment_button
+ end
+
+ it 'displays correct media bubble menu with edit diagram button' do
+ display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg'
+
+ expect_media_bubble_menu_to_be_visible
+
+ click_edit_diagram_button
+
+ expect_drawio_editor_is_opened
+ end
+end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 96e57980c68..7e0e235698e 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -5,20 +5,20 @@ RSpec.shared_examples 'a creatable merge request' do
include ListboxHelpers
it 'creates new merge request', :js do
- find('.js-assignee-search').click
+ find('[data-testid="assignee-ids-dropdown-toggle"]').click
page.within '.dropdown-menu-user' do
click_link user2.name
end
expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
- page.within '.js-assignee-search' do
+ page.within '[data-testid="assignee-ids-dropdown-toggle"]' do
expect(page).to have_content user2.name
end
click_link 'Assign to me'
expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
- page.within '.js-assignee-search' do
+ page.within '[data-testid="assignee-ids-dropdown-toggle"]' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
index efbd735c451..9b5d9d66890 100644
--- a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples "a dashboard page with sidebar" do |page_path, menu_label|
+RSpec.shared_examples 'a "Your work" page with sidebar and breadcrumbs' do |page_path, menu_label|
before do
sign_in(user)
visit send(page_path)
@@ -18,4 +18,13 @@ RSpec.shared_examples "a dashboard page with sidebar" do |page_path, menu_label|
expect(page).to have_css(active_menu_item_css)
end
end
+
+ describe "breadcrumbs" do
+ it 'has "Your work" as its root breadcrumb' do
+ breadcrumbs = page.find('[data-testid="breadcrumb-links"]')
+ within breadcrumbs do
+ expect(page).to have_css("li:first-child a[href=\"#{root_path}\"]", text: "Your work")
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/deploy_token_shared_examples.rb b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
index 9fe08e5c996..80f5f1d805c 100644
--- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb
+++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
@@ -17,9 +17,11 @@ RSpec.shared_examples 'a deploy token in settings' do
it 'add a new deploy token', :js do
visit page_path
- fill_in _('Name'), with: 'new_deploy_key'
- fill_in _('Expiration date (optional)'), with: (Date.today + 1.month).to_s
- fill_in _('Username (optional)'), with: 'deployer'
+ within('#js-deploy-tokens') do
+ fill_in _('Name'), with: 'new_deploy_key'
+ fill_in _('Expiration date (optional)'), with: (Date.today + 1.month).to_s
+ fill_in _('Username (optional)'), with: 'deployer'
+ end
check 'read_repository'
check 'read_registry'
click_button 'Create deploy token'
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index ea6d1655694..14e53dc8655 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -77,15 +77,21 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_selector('.js-quick-submit')
end
- it 'warns about version conflict' do
+ it 'warns about version conflict', :js do
merge_request.update!(title: "New title")
fill_in 'merge_request_title', with: 'bug 345'
fill_in 'merge_request_description', with: 'bug description'
- click_button 'Save changes'
+ click_button _('Save changes')
- expect(page).to have_content 'Someone edited the merge request the same time you did'
+ expect(page).to have_content(
+ format(
+ _("Someone edited this %{model_name} at the same time you did. Please check out the %{link_to_model} and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength
+ model_name: _('merge request'),
+ link_to_model: _('merge request')
+ )
+ )
end
it 'preserves description textarea height', :js do
@@ -104,8 +110,8 @@ RSpec.shared_examples 'an editable merge request' do
fill_in 'merge_request_description', with: long_description
height = get_textarea_height
- find('.js-md-preview-button').click
- find('.js-md-write-button').click
+ click_button("Preview")
+ click_button("Continue editing")
new_height = get_textarea_height
expect(height).to eq(new_height)
diff --git a/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb b/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb
new file mode 100644
index 00000000000..1754c8bf53d
--- /dev/null
+++ b/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'an "Explore" page with sidebar and breadcrumbs' do |page_path, menu_label|
+ before do
+ visit send(page_path)
+ end
+
+ let(:sidebar_css) { 'aside.nav-sidebar[aria-label="Explore"]' }
+ let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
+
+ it 'shows the "Explore" sidebar' do
+ expect(page).to have_css(sidebar_css)
+ end
+
+ it 'shows the correct sidebar menu item as active' do
+ within(sidebar_css) do
+ expect(page).to have_css(active_menu_item_css)
+ end
+ end
+
+ describe 'breadcrumbs' do
+ it 'has "Explore" as its root breadcrumb' do
+ within '.breadcrumbs-list' do
+ expect(page).to have_css("li:first a[href=\"#{explore_root_path}\"]", text: 'Explore')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
index dab125caa60..b8e42843e6f 100644
--- a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
+++ b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'for each incident details route' do |example, tab_text:|
+RSpec.shared_examples 'for each incident details route' do |example, tab_text:, tab:|
before do
sign_in(user)
visit incident_path
@@ -25,4 +25,16 @@ RSpec.shared_examples 'for each incident details route' do |example, tab_text:|
it_behaves_like example
end
+
+ context "for /-/issues/incident/:id/#{tab} route" do
+ let(:incident_path) { incident_project_issues_path(project, incident, tab) }
+
+ it_behaves_like example
+ end
+
+ context "for /-/issues/:id/#{tab} route" do
+ let(:incident_path) { incident_issue_project_issue_path(project, incident, tab) }
+
+ it_behaves_like example
+ end
end
diff --git a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
index 4c312b42c0a..148ff2cfb54 100644
--- a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
+++ b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'user activates the Mattermost Slash Command integration'
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
- expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ expect(token_placeholder).to eq('')
end
it 'redirects to the integrations page after saving but not activating' do
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index b6f7094e422..b8c6b85adb2 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable invite members' do
- include Spec::Support::Helpers::Features::InviteMembersModalHelper
+ include Features::InviteMembersModalHelpers
context 'when a privileged user can invite' do
before do
@@ -17,8 +17,6 @@ RSpec.shared_examples 'issuable invite members' do
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite Members')
- expect(page).to have_selector('[data-track-action="click_invite_members"]')
- expect(page).to have_selector('[data-track-label="edit_assignee"]')
end
click_link 'Invite Members'
diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
index b59f3f1e27b..b8fd58e7efa 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -5,87 +5,40 @@ RSpec.shared_examples 'manage applications' do
let_it_be(:application_name_changed) { "#{application_name} changed" }
let_it_be(:application_redirect_uri) { 'https://foo.bar' }
- context 'when hash_oauth_secrets flag set' do
- before do
- stub_feature_flags(hash_oauth_secrets: true)
- end
-
- it 'allows user to manage applications', :js do
- visit new_application_path
+ it 'allows user to manage applications', :js do
+ visit new_application_path
- expect(page).to have_content 'Add new application'
+ expect(page).to have_content 'Add new application'
- fill_in :doorkeeper_application_name, with: application_name
- fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
- check :doorkeeper_application_scopes_read_user
- click_on 'Save application'
+ fill_in :doorkeeper_application_name, with: application_name
+ fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
+ check :doorkeeper_application_scopes_read_user
+ click_on 'Save application'
- validate_application(application_name, 'Yes')
- expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely')
- expect(page).to have_link('Continue', href: index_path)
+ validate_application(application_name, 'Yes')
+ expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely')
+ expect(page).to have_link('Continue', href: index_path)
- expect(page).to have_css("button[title=\"Copy secret\"]", text: 'Copy')
+ expect(page).to have_button(_('Copy secret'))
- click_on 'Edit'
+ click_on 'Edit'
- application_name_changed = "#{application_name} changed"
+ application_name_changed = "#{application_name} changed"
- fill_in :doorkeeper_application_name, with: application_name_changed
- uncheck :doorkeeper_application_confidential
- click_on 'Save application'
-
- validate_application(application_name_changed, 'No')
- expect(page).not_to have_link('Continue')
- expect(page).to have_content _('The secret is only available when you first create the application')
-
- visit_applications_path
-
- page.within '.oauth-applications' do
- click_on 'Destroy'
- end
- expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
- end
- end
-
- context 'when hash_oauth_secrets flag not set' do
- before do
- stub_feature_flags(hash_oauth_secrets: false)
- end
-
- it 'allows user to manage applications', :js do
- visit new_application_path
-
- expect(page).to have_content 'Add new application'
-
- fill_in :doorkeeper_application_name, with: application_name
- fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
- check :doorkeeper_application_scopes_read_user
- click_on 'Save application'
-
- validate_application(application_name, 'Yes')
- expect(page).to have_link('Continue', href: index_path)
-
- application = Doorkeeper::Application.find_by(name: application_name)
- expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy')
-
- click_on 'Edit'
-
- application_name_changed = "#{application_name} changed"
-
- fill_in :doorkeeper_application_name, with: application_name_changed
- uncheck :doorkeeper_application_confidential
- click_on 'Save application'
+ fill_in :doorkeeper_application_name, with: application_name_changed
+ uncheck :doorkeeper_application_confidential
+ click_on 'Save application'
- validate_application(application_name_changed, 'No')
- expect(page).not_to have_link('Continue')
+ validate_application(application_name_changed, 'No')
+ expect(page).not_to have_link('Continue')
+ expect(page).to have_content _('The secret is only available when you create the application or renew the secret.')
- visit_applications_path
+ visit_applications_path
- page.within '.oauth-applications' do
- click_on 'Destroy'
- end
- expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
end
+ expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
end
context 'when scopes are blank' do
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
index c2dc87b0fb0..6487e6a94c1 100644
--- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'Maintainer manages access requests' do
- include Spec::Support::Helpers::Features::MembersHelpers
+ include Features::MembersHelpers
let(:user) { create(:user) }
let(:maintainer) { create(:user) }
diff --git a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
new file mode 100644
index 00000000000..d21bf62ecfa
--- /dev/null
+++ b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'milestone handling version conflicts' do
+ it 'warns about version conflict when milestone has been updated in the background' do
+ # Update the milestone in the background in order to trigger a version conflict
+ milestone.update!(title: "New title")
+
+ fill_in _('Title'), with: 'Title for version conflict'
+ fill_in _('Description'), with: 'Description for version conflict'
+
+ click_button _('Save changes')
+
+ expect(page).to have_content(
+ format(
+ _("Someone edited this %{model_name} at the same time you did. Please check out the %{link_to_model} and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength
+ model_name: _('milestone'),
+ link_to_model: _('milestone')
+ )
+ )
+ end
+end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index f09cf0613a1..5126e849c2e 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
expect(package_row).to have_content(pkg.name)
expect(package_row).to have_content(pkg.version)
- expect(package_row).to have_content(pkg.project.name) if check_project_name
+ expect(package_row).to have_content(pkg.project.path) if check_project_name
end
end
@@ -18,7 +18,35 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
end
+RSpec.shared_examples 'pipelines on packages list' do
+ let_it_be(:pipelines) do
+ %w[c83d6e391c22777fca1ed3012fce84f633d7fed0
+ d83d6e391c22777fca1ed3012fce84f633d7fed0].map do |sha|
+ create(:ci_pipeline, project: project, sha: sha)
+ end
+ end
+
+ before do
+ pipelines.each do |pipeline|
+ create(:package_build_info, package: package, pipeline: pipeline)
+ end
+ end
+
+ it 'shows the latest pipeline' do
+ # Test after reload
+ page.evaluate_script 'window.location.reload()'
+
+ wait_for_requests
+
+ expect(page).to have_content('d83d6e39')
+ end
+end
+
RSpec.shared_examples 'package details link' do |property|
+ before do
+ stub_application_setting(npm_package_requests_forwarding: false)
+ end
+
it 'navigates to the correct url' do
page.within(packages_table_selector) do
click_link package.name
@@ -30,6 +58,45 @@ RSpec.shared_examples 'package details link' do |property|
expect(page).to have_content('Installation')
expect(page).to have_content('Registry setup')
+ expect(page).to have_content('Other versions 0')
+ end
+
+ context 'with other versions' do
+ let_it_be(:npm_package1) { create(:npm_package, project: project, name: 'zzz', version: '1.1.0') }
+ let_it_be(:npm_package2) { create(:npm_package, project: project, name: 'zzz', version: '1.2.0') }
+
+ before do
+ page.within(packages_table_selector) do
+ first(:link, package.name).click
+ end
+ end
+
+ it 'shows tab with count' do
+ expect(page).to have_content('Other versions 2')
+ end
+
+ it 'visiting tab shows total on page' do
+ click_link 'Other versions'
+
+ expect(page).to have_content('2 versions')
+ end
+
+ it 'deleting version updates count' do
+ click_link 'Other versions'
+
+ find('[data-testid="delete-dropdown"]', match: :first).click
+ find('[data-testid="action-delete"]', match: :first).click
+ click_button('Permanently delete')
+
+ expect(page).to have_content 'Package deleted successfully'
+
+ expect(page).to have_content('Other versions 1')
+ expect(page).to have_content('1 version')
+
+ expect(page).not_to have_content('1.0.0')
+ expect(page).to have_content('1.1.0')
+ expect(page).to have_content('1.2.0')
+ end
end
end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
index 81d548e000a..2d3f1949716 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
@@ -1,126 +1,67 @@
# frozen_string_literal: true
RSpec.shared_examples "protected branches > access control > CE" do
- ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)|
+ let(:no_one) { ProtectedRef::AccessLevel.humanize(::Gitlab::Access::NO_ACCESS) }
+
+ ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can push to" do
visit project_protected_branches_path(project)
set_protected_branch_name('master')
-
- find(".js-allowed-to-merge").click
- within('[data-testid="allowed-to-merge-dropdown"]') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
-
- within('.js-new-protected-branch') do
- allowed_to_push_button = find(".js-allowed-to-push")
-
- unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.click
- within(".dropdown.show .dropdown-menu") { click_on access_type_name }
- end
- end
-
+ set_allowed_to('merge', no_one)
+ set_allowed_to('push', access_type_name)
click_on_protect
- wait_for_requests
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
end
- it "allows updating protected branches so that #{access_type_name} can push to them" do
+ it "allows creating protected branches that #{access_type_name} can merge to" do
visit project_protected_branches_path(project)
set_protected_branch_name('master')
-
- find(".js-allowed-to-merge").click
- within('[data-testid="allowed-to-merge-dropdown"]') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
-
- find(".js-allowed-to-push").click
- within('[data-testid="allowed-to-push-dropdown"]') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
-
+ set_allowed_to('merge', access_type_name)
+ set_allowed_to('push', no_one)
click_on_protect
expect(ProtectedBranch.count).to eq(1)
-
- within(".protected-branches-list") do
- find(".js-allowed-to-push").click
-
- within('.js-allowed-to-push-container') do
- expect(first("li")).to have_content("Roles")
- find(:link, access_type_name).click
- end
-
- find(".js-allowed-to-push").click
- end
-
- wait_for_requests
-
- expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
end
- end
- ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)|
- it "allows creating protected branches that #{access_type_name} can merge to" do
+ it "allows updating protected branches so that #{access_type_name} can push to them" do
visit project_protected_branches_path(project)
set_protected_branch_name('master')
+ set_allowed_to('merge', no_one)
+ set_allowed_to('push', no_one)
+ click_on_protect
- within('.js-new-protected-branch') do
- allowed_to_merge_button = find(".js-allowed-to-merge")
+ expect(ProtectedBranch.count).to eq(1)
- unless allowed_to_merge_button.text == access_type_name
- allowed_to_merge_button.click
- within(".dropdown.show .dropdown-menu") { click_on access_type_name }
+ within(".protected-branches-list") do
+ within_select(".js-allowed-to-push") do
+ click_on(access_type_name)
end
end
- find(".js-allowed-to-push").click
- within('[data-testid="allowed-to-push-dropdown"]') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
-
- click_on_protect
+ wait_for_requests
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
end
it "allows updating protected branches so that #{access_type_name} can merge to them" do
visit project_protected_branches_path(project)
set_protected_branch_name('master')
-
- find(".js-allowed-to-merge").click
- within('[data-testid="allowed-to-merge-dropdown"]') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
-
- find(".js-allowed-to-push").click
- within('[data-testid="allowed-to-push-dropdown"]') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
-
+ set_allowed_to('merge', no_one)
+ set_allowed_to('push', no_one)
click_on_protect
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
- find(".js-allowed-to-merge").click
-
- within('.js-allowed-to-merge-container') do
- expect(first("li")).to have_content("Roles")
- find(:link, access_type_name).click
+ within_select(".js-allowed-to-merge") do
+ click_on(access_type_name)
end
end
diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
new file mode 100644
index 00000000000..cc0984b6226
--- /dev/null
+++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Deploy keys with protected tags' do
+ let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] }
+
+ context 'when deploy keys are enabled to this project' do
+ let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) }
+ let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) }
+
+ context 'when only one deploy key can push' do
+ before do
+ deploy_key_1.deploy_keys_projects.first.update!(can_push: true)
+ end
+
+ it "shows all dropdown sections in the 'Allowed to create' main dropdown, with only one deploy key" do
+ visit project_protected_tags_path(project)
+
+ find(".js-allowed-to-create").click
+ wait_for_requests
+
+ within('[data-testid="allowed-to-create-dropdown"]') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
+ expect(page).to have_content('title 1')
+ expect(page).not_to have_content('title 2')
+ end
+ end
+
+ it "shows all sections in the 'Allowed to create' update dropdown" do
+ create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0')
+
+ visit project_protected_tags_path(project)
+
+ within(".js-protected-tag-edit-form") do
+ find(".js-allowed-to-create").click
+ wait_for_requests
+
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
+ end
+ end
+ end
+
+ context 'when no deploy key can push' do
+ it "just shows all sections but not deploy keys in the 'Allowed to create' dropdown" do
+ visit project_protected_tags_path(project)
+
+ find(".js-allowed-to-create").click
+ wait_for_requests
+
+ within('[data-testid="allowed-to-create-dropdown"]') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
index bb3fab5b23e..133da230bed 100644
--- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb
+++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
@@ -20,7 +20,7 @@ RSpec.shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_button('Report abuse to administrator')
+ expect(dropdown).to have_button('Report abuse')
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,7 +33,7 @@ RSpec.shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_button('Report abuse to administrator')
+ dropdown.click_button('Report abuse')
choose "They're posting spam."
click_button "Next"
@@ -48,6 +48,6 @@ RSpec.shared_examples 'reportable note' do |type|
restore_window_size
dropdown.find('.more-actions-toggle').click
- dropdown.find('.dropdown-menu li', match: :first)
+ dropdown.find('.more-actions li', match: :first)
end
end
diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb
index ad865b084e1..f6566214e32 100644
--- a/spec/support/shared_examples/features/rss_shared_examples.rb
+++ b/spec/support/shared_examples/features/rss_shared_examples.rb
@@ -13,6 +13,12 @@ RSpec.shared_examples "it has an RSS button with current_user's feed token" do
end
end
+RSpec.shared_examples "it has an RSS link with current_user's feed token" do
+ it "shows the RSS link with current_user's feed token" do
+ expect(page).to have_link 'Subscribe to RSS feed', href: /feed_token=#{user.feed_token}/
+ end
+end
+
RSpec.shared_examples "an autodiscoverable RSS feed without a feed token" do
it "has an RSS autodiscovery link tag without a feed token" do
expect(page).to have_css("link[type*='atom+xml']:not([href*='feed_token'])", visible: false)
@@ -26,10 +32,18 @@ RSpec.shared_examples "it has an RSS button without a feed token" do
end
end
+RSpec.shared_examples "it has an RSS link without a feed token" do
+ it "shows the RSS link without a feed token" do
+ expect(page).to have_link 'Subscribe to RSS feed'
+ expect(page).not_to have_link 'Subscribe to RSS feed', href: /feed_token/
+ end
+end
+
RSpec.shared_examples "updates atom feed link" do |type|
it "for #{type}" do
sign_in(user)
visit path
+ click_button 'Actions', match: :first
link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 63a0832117d..7edf306183e 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'shows and resets runner registration token' do
- include Spec::Support::Helpers::Features::RunnersHelpers
+ include Features::RunnersHelpers
include Spec::Support::Helpers::ModalHelpers
before do
@@ -63,16 +63,15 @@ RSpec.shared_examples 'shows and resets runner registration token' do
end
RSpec.shared_examples 'shows no runners registered' do
- it 'shows total count with 0' do
+ it 'shows 0 count and the empty state' do
expect(find('[data-testid="runner-type-tabs"]')).to have_text "#{s_('Runners|All')} 0"
# No stats are shown
expect(page).not_to have_text s_('Runners|Online')
expect(page).not_to have_text s_('Runners|Offline')
expect(page).not_to have_text s_('Runners|Stale')
- end
- it 'shows "no runners" message' do
+ # "no runners" message
expect(page).to have_text s_('Runners|Get started with runners')
end
end
@@ -84,16 +83,14 @@ RSpec.shared_examples 'shows no runners found' do
end
RSpec.shared_examples 'shows runner in list' do
- it 'does not show empty state' do
- expect(page).not_to have_content s_('Runners|Get started with runners')
- end
-
- it 'shows runner row' do
+ it 'shows runner row and no empty state' do
within_runner_row(runner.id) do
expect(page).to have_text "##{runner.id}"
expect(page).to have_text runner.short_sha
expect(page).to have_text runner.description
end
+
+ expect(page).not_to have_content s_('Runners|Get started with runners')
end
end
@@ -229,3 +226,33 @@ RSpec.shared_examples 'submits edit runner form' do
end
end
end
+
+RSpec.shared_examples 'creates runner and shows register page' do
+ context 'when runner is saved' do
+ before do
+ fill_in s_('Runners|Runner description'), with: 'runner-foo'
+ fill_in s_('Runners|Tags'), with: 'tag1'
+ click_on _('Submit')
+ wait_for_requests
+ end
+
+ it 'navigates to registration page and opens install instructions drawer' do
+ expect(page.find('[data-testid="alert-success"]')).to have_content(s_('Runners|Runner created.'))
+ expect(current_url).to match(register_path_pattern)
+
+ click_on 'How do I install GitLab Runner?'
+ expect(page.find('[data-testid="runner-platforms-drawer"]')).to have_content('gitlab-runner install')
+ end
+
+ it 'warns from leaving page without finishing registration' do
+ click_on s_('Runners|Go to runners page')
+
+ alert = page.driver.browser.switch_to.alert
+
+ expect(alert).not_to be_nil
+ alert.dismiss
+
+ expect(current_url).to match(register_path_pattern)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb
index 4d242d0e719..cbd0ffbab21 100644
--- a/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb
+++ b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb
@@ -48,14 +48,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible issue' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'Issue', id: unreadable.id, ability: :read_issue }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Issue', id: unreadable.id, ability: :read_issue }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
@@ -95,16 +99,18 @@ RSpec.shared_examples 'a redacted search results' do
end
let(:unredacted_results) do
- ar_relation(Note,
- readable_note_on_commit,
- readable_diff_note,
- readable_note_on_mr,
- readable_diff_note_on_mr,
- readable_note_on_project_snippet,
- unreadable_note_on_commit,
- unreadable_diff_note,
- unreadable_note_on_mr,
- unreadable_note_on_project_snippet)
+ ar_relation(
+ Note,
+ readable_note_on_commit,
+ readable_diff_note,
+ readable_note_on_mr,
+ readable_diff_note_on_mr,
+ readable_note_on_project_snippet,
+ unreadable_note_on_commit,
+ unreadable_diff_note,
+ unreadable_note_on_mr,
+ unreadable_note_on_project_snippet
+ )
end
let(:scope) { 'notes' }
@@ -112,23 +118,29 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible notes' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'Note', id: unreadable_note_on_commit.id, ability: :read_note },
- { class_name: 'DiffNote', id: unreadable_diff_note.id, ability: :read_note },
- { class_name: 'DiscussionNote', id: unreadable_note_on_mr.id, ability: :read_note },
- { class_name: 'Note', id: unreadable_note_on_project_snippet.id, ability: :read_note }
- ])))
-
- expect(result).to contain_exactly(readable_note_on_commit,
- readable_diff_note,
- readable_note_on_mr,
- readable_diff_note_on_mr,
- readable_note_on_project_snippet)
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Note', id: unreadable_note_on_commit.id, ability: :read_note },
+ { class_name: 'DiffNote', id: unreadable_diff_note.id, ability: :read_note },
+ { class_name: 'DiscussionNote', id: unreadable_note_on_mr.id, ability: :read_note },
+ { class_name: 'Note', id: unreadable_note_on_project_snippet.id, ability: :read_note }
+ ]
+ )
+ )
+ )
+
+ expect(result).to contain_exactly(
+ readable_note_on_commit,
+ readable_diff_note,
+ readable_note_on_mr,
+ readable_diff_note_on_mr,
+ readable_note_on_project_snippet
+ )
end
end
@@ -141,14 +153,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible merge request' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'MergeRequest', id: unreadable.id, ability: :read_merge_request }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'MergeRequest', id: unreadable.id, ability: :read_merge_request }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
@@ -169,14 +185,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible blob' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'Gitlab::Search::FoundBlob', id: unreadable.id, ability: :read_blob }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Gitlab::Search::FoundBlob', id: unreadable.id, ability: :read_blob }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
@@ -191,14 +211,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible blob' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'Gitlab::Search::FoundWikiPage', id: unreadable.id, ability: :read_wiki_page }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Gitlab::Search::FoundWikiPage', id: unreadable.id, ability: :read_wiki_page }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
@@ -213,14 +237,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible snippet' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'ProjectSnippet', id: unreadable.id, ability: :read_snippet }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'ProjectSnippet', id: unreadable.id, ability: :read_snippet }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
@@ -239,14 +267,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible snippet' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'PersonalSnippet', id: unreadable.id, ability: :read_snippet }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'PersonalSnippet', id: unreadable.id, ability: :read_snippet }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
@@ -265,14 +297,18 @@ RSpec.shared_examples 'a redacted search results' do
it 'redacts the inaccessible commit' do
expect(search_service.send(:logger))
.to receive(:error)
- .with(hash_including(
- message: "redacted_search_results",
- current_user_id: user.id,
- query: search,
- filtered: array_including(
- [
- { class_name: 'Commit', id: unreadable.id, ability: :read_commit }
- ])))
+ .with(
+ hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Commit', id: unreadable.id, ability: :read_commit }
+ ]
+ )
+ )
+ )
expect(result).to contain_exactly(readable)
end
diff --git a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb
index 028e075c87a..231406289b4 100644
--- a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb
+++ b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'Secure OAuth Authorizations' do
end
context 'when user is unconfirmed' do
- let(:user) { create(:user, confirmed_at: nil) }
+ let(:user) { create(:user, :unconfirmed) }
it 'displays an error' do
expect(page).to have_text I18n.t('doorkeeper.errors.messages.unconfirmed_email')
diff --git a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb
index 8304a91af86..81c9ac1164b 100644
--- a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb
+++ b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb
@@ -1,59 +1,38 @@
# frozen_string_literal: true
RSpec.shared_examples 'user email validation' do
- let(:email_hint_message) { 'We recommend a work email address.' }
- let(:email_error_message) { 'Please provide a valid email address.' }
+ let(:email_hint_message) { _('We recommend a work email address.') }
+ let(:email_error_message) { _('Please provide a valid email address.') }
let(:email_warning_message) do
- 'This email address does not look right, are you sure you typed it correctly?'
+ _('This email address does not look right, are you sure you typed it correctly?')
end
- context 'with trial_email_validation flag enabled' do
- it 'shows an error message until a correct email is entered' do
- visit path
- expect(page).to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
+ it 'shows an error message until a correct email is entered' do
+ visit path
+ expect(page).to have_content(email_hint_message)
+ expect(page).not_to have_content(email_error_message)
+ expect(page).not_to have_content(email_warning_message)
- fill_in 'new_user_email', with: 'foo@'
- fill_in 'new_user_first_name', with: ''
+ fill_in 'new_user_email', with: 'foo@'
+ fill_in 'new_user_first_name', with: ''
- expect(page).not_to have_content(email_hint_message)
- expect(page).to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
+ expect(page).not_to have_content(email_hint_message)
+ expect(page).to have_content(email_error_message)
+ expect(page).not_to have_content(email_warning_message)
- fill_in 'new_user_email', with: 'foo@bar'
- fill_in 'new_user_first_name', with: ''
+ fill_in 'new_user_email', with: 'foo@bar'
+ fill_in 'new_user_first_name', with: ''
- expect(page).not_to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).to have_content(email_warning_message)
+ expect(page).not_to have_content(email_hint_message)
+ expect(page).not_to have_content(email_error_message)
+ expect(page).to have_content(email_warning_message)
- fill_in 'new_user_email', with: 'foo@gitlab.com'
- fill_in 'new_user_first_name', with: ''
+ fill_in 'new_user_email', with: 'foo@gitlab.com'
+ fill_in 'new_user_first_name', with: ''
- expect(page).not_to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
- end
- end
-
- context 'when trial_email_validation flag disabled' do
- before do
- stub_feature_flags trial_email_validation: false
- end
-
- it 'does not show an error message' do
- visit path
- expect(page).to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
-
- fill_in 'new_user_email', with: 'foo@'
-
- expect(page).to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
- end
+ expect(page).not_to have_content(email_hint_message)
+ expect(page).not_to have_content(email_error_message)
+ expect(page).not_to have_content(email_warning_message)
end
end
diff --git a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb
new file mode 100644
index 00000000000..0b0c9edcb42
--- /dev/null
+++ b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'variable list pagination' do |variable_type|
+ first_page_count = 20
+
+ before do
+ first_page_count.times do |i|
+ case variable_type
+ when :ci_variable
+ create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, project: project)
+ when :ci_group_variable
+ create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, group: group)
+ else
+ create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true)
+ end
+ end
+
+ visit page_path
+ wait_for_requests
+ end
+
+ it 'can navigate between pages' do
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(page.all('.js-ci-variable-row').length).to be(first_page_count)
+ end
+
+ click_button 'Next'
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(page.all('.js-ci-variable-row').length).to be(1)
+ end
+
+ click_button 'Previous'
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(page.all('.js-ci-variable-row').length).to be(first_page_count)
+ end
+ end
+
+ it 'sorts variables alphabetically in ASC and DESC order' do
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
+ expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_8')
+ end
+
+ click_button 'Next'
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9')
+ end
+
+ page.within('[data-testid="ci-variable-table"]') do
+ find('.b-table-sort-icon-left').click
+ end
+
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9')
+ expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_0')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index f0b72cfaee3..1211c9d19e6 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'variable list' do |is_admin|
+RSpec.shared_examples 'variable list' do
it 'shows a list of variables' do
page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
@@ -256,14 +256,6 @@ RSpec.shared_examples 'variable list' do |is_admin|
expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked
end
end
-
- it 'shows a message regarding the changed default' do
- if is_admin
- expect(page).to have_content 'Environment variables on this GitLab instance are configured to be protected by default'
- else
- expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default'
- end
- end
end
context 'application setting is false' do
diff --git a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
index 7a3b94ad81d..6451c531aec 100644
--- a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
@@ -62,7 +62,7 @@ RSpec.shared_examples 'wiki file attachments' do
attach_with_dropzone(true)
wait_for_requests
- find('.js-md-preview-button').click
+ click_button("Preview")
file_path = page.find('input[name="files[]"]', visible: :hidden).value
link = page.find('a.no-attachment-icon')['href']
img_link = page.find('a.no-attachment-icon img')['src']
diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
index 3e285bb8ad7..ca68df9a89b 100644
--- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
@@ -78,7 +78,7 @@ RSpec.shared_examples 'User previews wiki changes' do
it_behaves_like 'relative links' do
before do
- click_on 'Preview'
+ click_button("Preview")
end
let(:element) { preview }
@@ -88,7 +88,7 @@ RSpec.shared_examples 'User previews wiki changes' do
# using two `\n` ensures we're sublist to it's own line due
# to list auto-continue
fill_in :wiki_content, with: "1. one\n\n - sublist\n"
- click_on "Preview"
+ click_button("Preview")
# the above generates two separate lists (not embedded) in CommonMark
expect(preview).to have_content("sublist")
@@ -102,7 +102,7 @@ RSpec.shared_examples 'User previews wiki changes' do
[[also_do_not_linkify]]
```
HEREDOC
- click_on "Preview"
+ click_button("Preview")
expect(preview).to have_content("do_not_linkify")
expect(preview).to have_content('[[do_not_linkify]]')
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index 0334187e4b1..c1e4185e058 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -150,6 +150,7 @@ RSpec.shared_examples 'User updates wiki page' do
end
it_behaves_like 'edits content using the content editor'
+ it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
index a7c32932ba7..767caffd417 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -9,9 +9,11 @@ RSpec.shared_examples 'User views a wiki page' do
let(:path) { 'image.png' }
let(:wiki_page) do
- create(:wiki_page,
- wiki: wiki,
- title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})")
+ create(
+ :wiki_page,
+ wiki: wiki,
+ title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})"
+ )
end
before do
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
index 639eb3f2b99..21c7e2b6c75 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
@@ -84,6 +84,44 @@ RSpec.shared_examples 'User views wiki sidebar' do
expect(page).not_to have_link('View All Pages')
end
+ it 'shows all collapse buttons in the sidebar' do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3)
+ end
+ end
+
+ it 'collapses/expands children when click collapse/expand button in the sidebar', :js do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ first("[data-testid='chevron-down-icon']").click
+ (11..15).each { |i| expect(page).not_to have_content("my page #{i}") }
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(1)
+ expect(page.all("[data-testid='chevron-right-icon']").size).to eq(1)
+
+ first("[data-testid='chevron-right-icon']").click
+ (11..15).each { |i| expect(page).to have_content("my page #{i}") }
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3)
+ expect(page.all("[data-testid='chevron-right-icon']").size).to eq(0)
+ end
+ end
+
+ it 'shows create child page button when hover to the page title in the sidebar', :js do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ first_wiki_list = first("[data-testid='wiki-list']")
+ wiki_link = first("[data-testid='wiki-list'] a:last-of-type")['href']
+
+ first_wiki_list.hover
+ wiki_new_page_link = first("[data-testid='wiki-list'] a")['href']
+
+ expect(wiki_new_page_link).to eq "#{wiki_link}/%7Bnew_page_title%7D"
+ end
+ end
+
context 'when there are more than 15 existing pages' do
before do
create(:wiki_page, wiki: wiki, title: 'my page 16')
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 4f3d957ad71..526a56e7dab 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -1,5 +1,20 @@
# frozen_string_literal: true
+RSpec.shared_examples 'work items title' do
+ let(:title_selector) { '[data-testid="work-item-title"]' }
+
+ it 'successfully shows and changes the title of the work item' do
+ expect(work_item.reload.title).to eq work_item.title
+
+ find(title_selector).set("Work item title")
+ find(title_selector).native.send_keys(:return)
+
+ wait_for_requests
+
+ expect(work_item.reload.title).to eq 'Work item title'
+ end
+end
+
RSpec.shared_examples 'work items status' do
let(:state_selector) { '[data-testid="work-item-state-select"]' }
@@ -15,18 +30,110 @@ RSpec.shared_examples 'work items status' do
end
end
-RSpec.shared_examples 'work items comments' do
+RSpec.shared_examples 'work items comments' do |type|
let(:form_selector) { '[data-testid="work-item-add-comment"]' }
+ let(:textarea_selector) { '[data-testid="work-item-add-comment"] #work-item-add-or-edit-comment' }
+ let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
+ let(:modifier_key) { is_mac ? :command : :control }
+ let(:comment) { 'Test comment' }
+
+ def set_comment
+ find(form_selector).fill_in(with: comment)
+ end
it 'successfully creates and shows comments' do
- click_button 'Add a comment'
+ set_comment
- find(form_selector).fill_in(with: "Test comment")
click_button "Comment"
wait_for_requests
- expect(page).to have_content "Test comment"
+ page.within(".main-notes-list") do
+ expect(page).to have_content comment
+ end
+ end
+
+ context 'for work item note actions signed in user with developer role' do
+ it 'shows work item note actions' do
+ set_comment
+
+ click_button "Comment"
+
+ wait_for_requests
+
+ page.within(".main-notes-list") do
+ expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+
+ find('[data-testid="work-item-note-actions"]', match: :first).click
+
+ expect(page).to have_selector('[data-testid="copy-link-action"]')
+ expect(page).not_to have_selector('[data-testid="assign-note-action"]')
+ end
+ end
+ end
+
+ it 'successfully posts comments using shortcut and checks if textarea is blank when reinitiated' do
+ set_comment
+
+ send_keys([modifier_key, :enter])
+
+ wait_for_requests
+
+ page.within(".main-notes-list") do
+ expect(page).to have_content comment
+ end
+
+ expect(find(textarea_selector)).to have_content ""
+ end
+
+ context 'when using quick actions' do
+ it 'autocompletes quick actions common to all work item types', :aggregate_failures do
+ click_reply_and_enter_slash
+
+ page.within('#at-view-commands') do
+ expect(page).to have_text("/title")
+ expect(page).to have_text("/shrug")
+ expect(page).to have_text("/tableflip")
+ expect(page).to have_text("/close")
+ expect(page).to have_text("/cc")
+ end
+ end
+
+ context 'when a widget is enabled' do
+ before do
+ WorkItems::Type.default_by_type(type).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: false)
+ end
+
+ it 'autocompletes quick action for the enabled widget' do
+ click_reply_and_enter_slash
+
+ page.within('#at-view-commands') do
+ expect(page).to have_text("/assign")
+ end
+ end
+ end
+
+ context 'when a widget is disabled' do
+ before do
+ WorkItems::Type.default_by_type(type).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: true)
+ end
+
+ it 'does not autocomplete quick action for the disabled widget' do
+ click_reply_and_enter_slash
+
+ page.within('#at-view-commands') do
+ expect(page).not_to have_text("/assign")
+ end
+ end
+ end
+
+ def click_reply_and_enter_slash
+ find(form_selector).fill_in(with: "/")
+
+ wait_for_all_requests
+ end
end
end
@@ -39,7 +146,6 @@ RSpec.shared_examples 'work items assignees' do
# submit and simulate blur to save
send_keys(:enter)
find("body").click
-
wait_for_requests
expect(work_item.assignees).to include(user)
@@ -47,6 +153,8 @@ RSpec.shared_examples 'work items assignees' do
end
RSpec.shared_examples 'work items labels' do
+ let(:label_title_selector) { '[data-testid="labels-title"]' }
+
it 'successfully assigns a label' do
label = create(:label, project: work_item.project, title: "testing-label")
@@ -55,8 +163,7 @@ RSpec.shared_examples 'work items labels' do
# submit and simulate blur to save
send_keys(:enter)
- find("body").click
-
+ find(label_title_selector).click
wait_for_requests
expect(work_item.labels).to include(label)
@@ -83,7 +190,7 @@ RSpec.shared_examples 'work items description' do
wait_for_requests
- page.within('.atwho-container') do
+ page.within('#at-view-commands') do
expect(page).to have_text("title")
expect(page).to have_text("shrug")
expect(page).to have_text("tableflip")
@@ -125,7 +232,7 @@ RSpec.shared_examples 'work items description' do
end
RSpec.shared_examples 'work items invite members' do
- include Spec::Support::Helpers::Features::InviteMembersModalHelper
+ include Features::InviteMembersModalHelpers
it 'successfully assigns the current user by searching' do
# The button is only when the mouse is over the input
@@ -139,3 +246,143 @@ RSpec.shared_examples 'work items invite members' do
end
end
end
+
+RSpec.shared_examples 'work items milestone' do
+ def set_milestone(milestone_dropdown, milestone_text)
+ milestone_dropdown.click
+
+ find('[data-testid="work-item-milestone-dropdown"] .gl-form-input', visible: true).send_keys "\"#{milestone_text}\""
+ wait_for_requests
+
+ click_button(milestone_text)
+ wait_for_requests
+ end
+
+ let(:milestone_dropdown_selector) { '[data-testid="work-item-milestone-dropdown"]' }
+
+ it 'searches and sets or removes milestone for the work item' do
+ set_milestone(find(milestone_dropdown_selector), milestone.title)
+
+ expect(page.find(milestone_dropdown_selector)).to have_text(milestone.title)
+
+ set_milestone(find(milestone_dropdown_selector), 'No milestone')
+
+ expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone')
+ end
+end
+
+RSpec.shared_examples 'work items comment actions for guest users' do
+ context 'for guest user' do
+ it 'hides other actions other than copy link' do
+ page.within(".main-notes-list") do
+ expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+
+ find('[data-testid="work-item-note-actions"]', match: :first).click
+
+ expect(page).to have_selector('[data-testid="copy-link-action"]')
+ expect(page).not_to have_selector('[data-testid="assign-note-action"]')
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'work items notifications' do
+ let(:actions_dropdown_selector) { '[data-testid="work-item-actions-dropdown"]' }
+ let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] > button' }
+
+ it 'displays toast when notification is toggled' do
+ find(actions_dropdown_selector).click
+
+ page.within('[data-testid="notifications-toggle-form"]') do
+ expect(page).not_to have_css(".is-checked")
+
+ find(notifications_toggle_selector).click
+ wait_for_requests
+
+ expect(page).to have_css(".is-checked")
+ end
+
+ page.within('.gl-toast') do
+ expect(find('.toast-body')).to have_content(_('Notifications turned on.'))
+ end
+ end
+end
+
+RSpec.shared_examples 'work items todos' do
+ let(:todos_action_selector) { '[data-testid="work-item-todos-action"]' }
+ let(:todos_icon_selector) { '[data-testid="work-item-todos-icon"]' }
+ let(:header_section_selector) { '[data-testid="work-item-body"]' }
+
+ def toggle_todo_action
+ find(todos_action_selector).click
+ wait_for_requests
+ end
+
+ it 'adds item to the list' do
+ page.within(header_section_selector) do
+ expect(find(todos_action_selector)['aria-label']).to eq('Add a to do')
+
+ toggle_todo_action
+
+ expect(find(todos_action_selector)['aria-label']).to eq('Mark as done')
+ end
+
+ page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ expect(page).to have_content '1'
+ end
+ end
+
+ it 'marks a todo as done' do
+ page.within(header_section_selector) do
+ toggle_todo_action
+ toggle_todo_action
+ end
+
+ expect(find(todos_action_selector)['aria-label']).to eq('Add a to do')
+ expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: :hidden)
+ end
+end
+
+RSpec.shared_examples 'work items award emoji' do
+ let(:award_section_selector) { '[data-testid="work-item-award-list"]' }
+ let(:award_action_selector) { '[data-testid="award-button"]' }
+ let(:selected_award_action_selector) { '[data-testid="award-button"].selected' }
+ let(:emoji_picker_action_selector) { '[data-testid="emoji-picker"]' }
+ let(:basketball_emoji_selector) { 'gl-emoji[data-name="basketball"]' }
+
+ def select_emoji
+ first(award_action_selector).click
+
+ wait_for_requests
+ end
+
+ it 'adds award to the work item' do
+ within(award_section_selector) do
+ select_emoji
+
+ expect(page).to have_selector(selected_award_action_selector)
+ expect(first(award_action_selector)).to have_content '1'
+ end
+ end
+
+ it 'removes award from work item' do
+ within(award_section_selector) do
+ select_emoji
+
+ expect(first(award_action_selector)).to have_content '1'
+
+ select_emoji
+
+ expect(first(award_action_selector)).to have_content '0'
+ end
+ end
+
+ it 'add custom award to the work item' do
+ within(award_section_selector) do
+ find(emoji_picker_action_selector).click
+ find(basketball_emoji_selector).click
+
+ expect(page).to have_selector(basketball_emoji_selector)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 93f9e42241b..67fed00b5ca 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -161,10 +161,12 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
let_it_be(:another_release) { create(:release, project: project1, tag: 'v2.0.0') }
let_it_be(:another_milestone) { create(:milestone, project: project1, releases: [another_release]) }
let_it_be(:another_item) do
- create(factory,
- project: project1,
- milestone: another_milestone,
- title: 'another item')
+ create(
+ factory,
+ project: project1,
+ milestone: another_milestone,
+ title: 'another item'
+ )
end
let(:params) { { not: { release_tag: release.tag, project_id: project1.id } } }
@@ -421,8 +423,11 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
let!(:created_items) do
milestones.map do |milestone|
- create(factory, project: milestone.project || project_in_group,
- milestone: milestone, author: user, assignees: [user])
+ create(
+ factory,
+ project: milestone.project || project_in_group,
+ milestone: milestone, author: user, assignees: [user]
+ )
end
end
@@ -593,7 +598,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'filtering by no label' do
- let(:params) { { label_name: described_class::Params::FILTER_NONE } }
+ let(:params) { { label_name: IssuableFinder::Params::FILTER_NONE } }
it 'returns items with no labels' do
expect(items).to contain_exactly(item1, item4, item5)
@@ -601,7 +606,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'filtering by any label' do
- let(:params) { { label_name: described_class::Params::FILTER_ANY } }
+ let(:params) { { label_name: IssuableFinder::Params::FILTER_ANY } }
it 'returns items that have one or more label' do
create_list(:label_link, 2, label: create(:label, project: project2), target: item3)
@@ -909,9 +914,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'filtering by item type' do
- let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) }
- let_it_be(:objective) { create(factory, issue_type: :objective, project: project1) }
- let_it_be(:key_result) { create(factory, issue_type: :key_result, project: project1) }
+ let_it_be(:incident_item) { create(factory, :incident, project: project1) }
+ let_it_be(:objective) { create(factory, :objective, project: project1) }
+ let_it_be(:key_result) { create(factory, :key_result, project: project1) }
context 'no type given' do
let(:params) { { issue_types: [] } }
@@ -983,9 +988,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:project_crm) { create(:project, :public, group: group) }
- let_it_be(:organization) { create(:organization, group: root_group) }
- let_it_be(:contact1) { create(:contact, group: root_group, organization: organization) }
- let_it_be(:contact2) { create(:contact, group: root_group, organization: organization) }
+ let_it_be(:crm_organization) { create(:crm_organization, group: root_group) }
+ let_it_be(:contact1) { create(:contact, group: root_group, organization: crm_organization) }
+ let_it_be(:contact2) { create(:contact, group: root_group, organization: crm_organization) }
let_it_be(:contact1_item1) { create(factory, project: project_crm) }
let_it_be(:contact1_item2) { create(factory, project: project_crm) }
@@ -1023,10 +1028,10 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'filtering by crm organization' do
- let(:params) { { project_id: project_crm.id, crm_organization_id: organization.id } }
+ let(:params) { { project_id: project_crm.id, crm_organization_id: crm_organization.id } }
context 'when the user can read crm organization' do
- it 'returns for that organization' do
+ it 'returns for that crm organization' do
root_group.add_reporter(user)
expect(items).to contain_exactly(contact1_item1, contact1_item2, contact2_item1)
@@ -1034,7 +1039,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'when the user can not read crm organization' do
- it 'does not filter by organization' do
+ it 'does not filter by crm organization' do
expect(items).to match_array(all_project_issues)
end
end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index 5cba8baa829..5ab17f5a49d 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -39,8 +39,10 @@ RSpec.shared_examples 'querying members with a group' do
let(:base_args) { { relations: described_class.arguments['relations'].default_value } }
subject do
- resolve(described_class, obj: resource, args: base_args.merge(args),
- ctx: { current_user: user_4 }, arg_style: :internal)
+ resolve(
+ described_class, obj: resource, args: base_args.merge(args),
+ ctx: { current_user: user_4 }, arg_style: :internal
+ )
end
describe '#resolve' do
@@ -83,8 +85,10 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:other_user) { create(:user) }
subject do
- resolve(described_class, obj: resource, args: base_args.merge(args),
- ctx: { current_user: other_user }, arg_style: :internal)
+ resolve(
+ described_class, obj: resource, args: base_args.merge(args),
+ ctx: { current_user: other_user }, arg_style: :internal
+ )
end
it 'generates an error' do
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
index dc590e23ace..808fb097f29 100644
--- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []|
expect(graphql_errors).to be_present
- error_messages = graphql_errors.map { |e| e['message'] }
+ error_messages = graphql_errors.pluck('message')
expect(error_messages).to match_errors
end
@@ -25,7 +25,7 @@ end
# the mutation.
RSpec.shared_examples 'a mutation that returns a top-level access error' do
include_examples 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
RSpec.shared_examples 'an invalid argument to the mutation' do |argument_name:|
diff --git a/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb
new file mode 100644
index 00000000000..e885b5d283e
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'members bulk update mutation' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:member1) { create(member_type, source: source, user: user1) }
+ let_it_be(:member2) { create(member_type, source: source, user: user2) }
+
+ let(:extra_params) { { expires_at: 10.days.from_now } }
+ let(:input_params) { input.merge(extra_params) }
+ let(:mutation) { graphql_mutation(mutation_name, input_params) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name) }
+
+ let(:input) do
+ {
+ source_id_key => source.to_global_id.to_s,
+ 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
+ 'access_level' => 'GUEST'
+ }
+ end
+
+ context 'when user is not logged-in' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user is not an owner' do
+ before do
+ source.add_developer(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user is an owner' do
+ before do
+ source.add_owner(current_user)
+ end
+
+ shared_examples 'updates the user access role' do
+ specify do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ new_access_levels = mutation_response[response_member_field].map do |member|
+ member['accessLevel']['integerValue']
+ end
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(new_access_levels).to all(be Gitlab::Access::GUEST)
+ end
+ end
+
+ it_behaves_like 'updates the user access role'
+
+ context 'when inherited members are passed' do
+ let(:input) do
+ {
+ source_id_key => source.to_global_id.to_s,
+ 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, parent_group_member.user.to_global_id.to_s],
+ 'access_level' => 'GUEST'
+ }
+ end
+
+ it 'does not update the members' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ error = Mutations::Members::BulkUpdateBase::INVALID_MEMBERS_ERROR
+ expect(json_response['errors'].first['message']).to include(error)
+ end
+ end
+
+ context 'when members count is more than the allowed limit' do
+ let(:max_members_update_limit) { 1 }
+
+ before do
+ stub_const('Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
+ end
+
+ it 'does not update the members' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ error = Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_ERROR
+ expect(json_response['errors'].first['message']).to include(error)
+ end
+ end
+
+ context 'when the update service raises access denied error' do
+ before do
+ allow_next_instance_of(Members::UpdateService) do |instance|
+ allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ it 'does not update the members' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response[response_member_field]).to be_nil
+ expect(mutation_response['errors'])
+ .to contain_exactly("Unable to update members, please check user permissions.")
+ end
+ end
+
+ context 'when the update service returns an error message' do
+ before do
+ allow_next_instance_of(Members::UpdateService) do |instance|
+ error_result = {
+ message: 'Expires at cannot be a date in the past',
+ status: :error,
+ members: [member1]
+ }
+ allow(instance).to receive(:execute).and_return(error_result)
+ end
+ end
+
+ it 'will pass through the error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response[response_member_field].first['id']).to eq(member1.to_global_id.to_s)
+ expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb
index 022e2308517..3b9dadf2e80 100644
--- a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb
@@ -16,10 +16,12 @@ RSpec.shared_examples 'an assignable resource' do
let(:mode) { described_class.arguments['operationMode'].default_value }
subject do
- mutation.resolve(project_path: resource.project.full_path,
- iid: resource.iid,
- operation_mode: mode,
- assignee_usernames: assignee_usernames)
+ mutation.resolve(
+ project_path: resource.project.full_path,
+ iid: resource.iid,
+ operation_mode: mode,
+ assignee_usernames: assignee_usernames
+ )
end
it 'raises an error if the resource is not accessible to the user' do
diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
index 0d2e9f6ec8c..99d122e8254 100644
--- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
@@ -4,9 +4,11 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do
include GraphqlHelpers
let(:note) do
- create(:note,
- noteable: noteable,
- project: (noteable.project if noteable.respond_to?(:project)))
+ create(
+ :note,
+ noteable: noteable,
+ project: (noteable.project if noteable.respond_to?(:project))
+ )
end
let(:user) { note.author }
@@ -46,7 +48,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do
discussions {
edges {
node {
- #{all_graphql_fields_for('Discussion', max_depth: 4)}
+ #{all_graphql_fields_for('Discussion', max_depth: 4, excluded: ['productAnalyticsState'])}
}
}
}
diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
new file mode 100644
index 00000000000..52908c5b6df
--- /dev/null
+++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
@@ -0,0 +1,195 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work item supports assignee widget updates via quick actions' do
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+
+ context 'when assigning a user' do
+ let(:body) { "/assign @#{developer.username}" }
+
+ it 'updates the work item assignee' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.assignee_ids }.from([]).to([developer.id])
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'when unassigning a user' do
+ let(:body) { "/unassign @#{developer.username}" }
+
+ before do
+ noteable.update!(assignee_ids: [developer.id])
+ end
+
+ it 'updates the work item assignee' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.assignee_ids }.from([developer.id]).to([])
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+end
+
+RSpec.shared_examples 'work item does not support assignee widget updates via quick actions' do
+ let(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+ let(:body) { "Updating assignee.\n/assign @#{developer.username}" }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.assignee_ids }
+ end
+end
+
+RSpec.shared_examples 'work item supports labels widget updates via quick actions' do
+ shared_examples 'work item labels are updated' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.labels.count }.to(expected_labels.count)
+
+ expect(noteable.labels).to match_array(expected_labels)
+ end
+ end
+
+ let_it_be(:existing_label) { create(:label, project: project) }
+ let_it_be(:label1) { create(:label, project: project) }
+ let_it_be(:label2) { create(:label, project: project) }
+
+ let(:add_label_ids) { [] }
+ let(:remove_label_ids) { [] }
+
+ before_all do
+ noteable.update!(labels: [existing_label])
+ end
+
+ context 'when only removing labels' do
+ let(:remove_label_ids) { [existing_label.to_gid.to_s] }
+ let(:expected_labels) { [] }
+ let(:body) { "/remove_label ~\"#{existing_label.name}\"" }
+
+ it_behaves_like 'work item labels are updated'
+ end
+
+ context 'when only adding labels' do
+ let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] }
+ let(:expected_labels) { [label1, label2, existing_label] }
+ let(:body) { "/labels ~\"#{label1.name}\" ~\"#{label2.name}\"" }
+
+ it_behaves_like 'work item labels are updated'
+ end
+
+ context 'when adding and removing labels' do
+ let(:remove_label_ids) { [existing_label.to_gid.to_s] }
+ let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] }
+ let(:expected_labels) { [label1, label2] }
+ let(:body) { "/label ~\"#{label1.name}\" ~\"#{label2.name}\"\n/remove_label ~\"#{existing_label.name}\"" }
+
+ it_behaves_like 'work item labels are updated'
+ end
+end
+
+RSpec.shared_examples 'work item does not support labels widget updates via quick actions' do
+ let(:label1) { create(:label, project: project) }
+ let(:body) { "Updating labels.\n/labels ~\"#{label1.name}\"" }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:labels).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.labels.count }
+
+ expect(noteable.labels).to be_empty
+ end
+end
+
+RSpec.shared_examples 'work item supports start and due date widget updates via quick actions' do
+ let(:due_date) { Date.today }
+ let(:body) { "/remove_due_date" }
+
+ before do
+ noteable.update!(due_date: due_date)
+ end
+
+ it 'updates start and due date' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to not_change(noteable, :start_date).and(
+ change { noteable.due_date }.from(due_date).to(nil)
+ )
+ end
+end
+
+RSpec.shared_examples 'work item does not support start and due date widget updates via quick actions' do
+ let(:body) { "Updating due date.\n/due today" }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:start_and_due_date).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.due_date }
+ end
+end
+
+RSpec.shared_examples 'work item supports type change via quick actions' do
+ let_it_be(:assignee) { create(:user) }
+ let_it_be(:task_type) { WorkItems::Type.default_by_type(:task) }
+
+ let(:body) { "Updating type.\n/type Issue" }
+
+ before do
+ noteable.update!(work_item_type: task_type, issue_type: task_type.base_type)
+ end
+
+ it 'updates type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ context 'when quick command for unsupported widget is present' do
+ let(:body) { "\n/type Issue\n/assign @#{assignee.username}" }
+
+ before do
+ WorkItems::Type.default_by_type(:issue).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: true)
+ end
+
+ it 'updates only type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
+ .and change { noteable.assignees }.to([])
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors'])
+ .to include("Commands only Type changed successfully. Assigned @#{assignee.username}.")
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
new file mode 100644
index 00000000000..8551bd052ce
--- /dev/null
+++ b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Data transfer resolver' do
+ it 'returns mock data' do |_query_object|
+ mocked_data = ['mocked_data']
+
+ allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance|
+ allow(instance).to receive(:execute).and_return(mocked_data)
+ end
+
+ expect(resolve_egress[:egress_nodes]).to eq(mocked_data)
+ end
+
+ context 'when data_transfer_monitoring is disabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring: false)
+ end
+
+ it 'returns empty result' do
+ expect(resolve_egress).to eq(egress_nodes: [])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
index 4dc2ce61c4d..b346f35bdc9 100644
--- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -65,7 +65,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha })
expect(deprecable.deprecation_reason).to eq(
- 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.'
+ 'This feature is an Experiment. It can be changed or removed at any time. Introduced in 1.10.'
)
end
@@ -73,7 +73,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
deprecable = subject(alpha: { milestone: '1.10' })
expect(deprecable.deprecation_reason).to eq(
- 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.'
+ 'This feature is an Experiment. It can be changed or removed at any time. Introduced in 1.10.'
)
end
@@ -82,7 +82,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
subject(alpha: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } )
end.to raise_error(
ArgumentError,
- eq("`alpha` and `deprecated` arguments cannot be passed at the same time")
+ eq("`experiment` and `deprecated` arguments cannot be passed at the same time")
)
end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index bb33a7559dc..3dffc2066ae 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -42,8 +42,13 @@ RSpec.shared_examples "a user type with merge request interaction type" do
profileEnableGitpodPath
savedReplies
savedReply
+ user_achievements
]
+ # TODO: 'workspaces' needs to be included, but only when this spec is run in EE context, to account for the
+ # ee-only extension in ee/app/graphql/ee/types/user_interface.rb. Not sure how else to handle this.
+ expected_fields << 'workspaces' if Gitlab.ee?
+
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb
new file mode 100644
index 00000000000..b3d3000aa06
--- /dev/null
+++ b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'CalloutsHelper#web_hook_disabled_dismissed shared examples' do
+ context 'when the web-hook failure callout has never been dismissed' do
+ it 'is false' do
+ expect(helper).not_to be_web_hook_disabled_dismissed(container)
+ end
+ end
+
+ context 'when the web-hook failure callout has been dismissed', :freeze_time, :clean_gitlab_redis_shared_state do
+ before do
+ create(factory,
+ feature_name: Users::CalloutsHelper::WEB_HOOK_DISABLED,
+ user: user,
+ dismissed_at: 1.week.ago,
+ container_key => container)
+ end
+
+ it 'is true' do
+ expect(helper).to be_web_hook_disabled_dismissed(container)
+ end
+
+ it 'is true when passed as a presenter' do
+ skip "Does not apply to #{container.class}" unless container.is_a?(Presentable)
+
+ expect(helper).to be_web_hook_disabled_dismissed(container.present)
+ end
+
+ context 'when there was an older failure' do
+ before do
+ Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) }
+ end
+
+ it 'is true' do
+ expect(helper).to be_web_hook_disabled_dismissed(container)
+ end
+ end
+
+ context 'when there has been a more recent failure' do
+ before do
+ Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) }
+ end
+
+ it 'is false' do
+ expect(helper).not_to be_web_hook_disabled_dismissed(container)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
index aeb4e0feb12..c43bdfa53ff 100644
--- a/spec/support/shared_examples/integrations/integration_settings_form.rb
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -2,12 +2,16 @@
RSpec.shared_examples 'integration settings form' do
include IntegrationsHelper
+
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
# Note: these specs don't validate channel fields
# which are present on a few integrations
it 'displays all the integrations', feature_category: :integrations do
aggregate_failures do
integrations.each do |integration|
- stub_feature_flags(integration_slack_app_notifications: false)
navigate_to_integration(integration)
page.within('form.integration-settings-form') do
diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
new file mode 100644
index 00000000000..7ace223723c
--- /dev/null
+++ b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag|
+ context "when #{provider_flag} is disabled" do
+ before do
+ stub_feature_flags(provider_flag => false)
+ end
+
+ it 'responds as not found' do
+ post api(url, current_user), params: input_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when ai_experimentation_api is disabled' do
+ before do
+ stub_feature_flags(ai_experimentation_api: false)
+ end
+
+ it 'responds as not found' do
+ post api(url, current_user), params: input_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it 'responds with Workhorse send-url headers' do
+ post api(url, current_user), params: input_params
+
+ expect(response.body).to eq('""')
+ expect(response).to have_gitlab_http_status(:ok)
+
+ send_url_prefix, encoded_data = response.headers['Gitlab-Workhorse-Send-Data'].split(':')
+ data = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_data))
+
+ expect(send_url_prefix).to eq('send-url')
+ expect(data).to eq({
+ 'AllowRedirects' => false,
+ 'Method' => 'POST'
+ }.merge(expected_params))
+ end
+end
diff --git a/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb
new file mode 100644
index 00000000000..b88eade7db2
--- /dev/null
+++ b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'it depends on value of the `terraform_state.enabled` config' do |params = {}|
+ let(:expected_success_status) { params[:success_status] || :ok }
+
+ context 'when terraform_state.enabled=false' do
+ before do
+ stub_config(terraform_state: { enabled: false })
+ end
+
+ it 'returns `forbidden` response' do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when terraform_state.enabled=true' do
+ before do
+ stub_config(terraform_state: { enabled: true })
+ end
+
+ it 'returns a successful response' do
+ request
+
+ expect(response).to have_gitlab_http_status(expected_success_status)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
index d471a758f3e..c8d62205c1e 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
@@ -1,14 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'deployment metrics examples' do
- def create_deployment(args)
- project = args[:project]
- environment = project.environments.production.first || create(:environment, :production, project: project)
- create(:deployment, :success, args.merge(environment: environment))
-
- # this is needed for the DORA API so we have aggregated data
- ::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee?
- end
+ include CycleAnalyticsHelpers
describe "#deploys" do
subject { stage_summary.third }
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb
index bce889b454d..5740adb3f0e 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb
@@ -68,3 +68,64 @@ RSpec.shared_examples_for 'LEFT JOIN-able value stream analytics event' do
end
end
end
+
+RSpec.shared_examples_for 'value stream analytics first assignment event methods' do
+ let_it_be(:model1) { create(model_factory) } # rubocop: disable Rails/SaveBang
+ let_it_be(:model2) { create(model_factory) } # rubocop: disable Rails/SaveBang
+
+ let_it_be(:assignment_event1) do
+ create(event_factory, action: :add, created_at: 3.years.ago, model_factory => model1)
+ end
+
+ let_it_be(:assignment_event2) do
+ create(event_factory, action: :add, created_at: 2.years.ago, model_factory => model1)
+ end
+
+ let_it_be(:unassignment_event1) do
+ create(event_factory, action: :remove, created_at: 1.year.ago, model_factory => model1)
+ end
+
+ let(:query) { model1.class.where(id: [model1.id, model2.id]) }
+ let(:event) { described_class.new({}) }
+
+ describe '#apply_query_customization' do
+ subject(:records) { event.apply_query_customization(query).pluck(:id, *event.column_list).to_a }
+
+ it 'looks up the first assignment event timestamp' do
+ expect(records).to match_array([[model1.id, be_within(1.second).of(assignment_event1.created_at)]])
+ end
+ end
+
+ describe '#apply_negated_query_customization' do
+ subject(:records) { event.apply_negated_query_customization(query).pluck(:id).to_a }
+
+ it 'returns records where the event has not happened yet' do
+ expect(records).to eq([model2.id])
+ end
+ end
+
+ describe '#include_in' do
+ subject(:records) { event.include_in(query).pluck(:id, *event.column_list).to_a }
+
+ it 'returns both records' do
+ expect(records).to match_array([
+ [model1.id, be_within(1.second).of(assignment_event1.created_at)],
+ [model2.id, nil]
+ ])
+ end
+
+ context 'when invoked multiple times' do
+ subject(:records) do
+ scope = event.include_in(query)
+ event.include_in(scope).pluck(:id, *event.column_list).to_a
+ end
+
+ it 'returns both records' do
+ expect(records).to match_array([
+ [model1.id, be_within(1.second).of(assignment_event1.created_at)],
+ [model2.id, nil]
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb
new file mode 100644
index 00000000000..b9d71183851
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'async constraints validation' do
+ include ExclusiveLeaseHelpers
+
+ let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION }
+
+ let(:constraints_model) { Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation }
+ let(:table_name) { '_test_async_constraints' }
+ let(:constraint_name) { 'constraint_parent_id' }
+
+ let(:validation) do
+ create(:postgres_async_constraint_validation,
+ table_name: table_name,
+ name: constraint_name,
+ constraint_type: constraint_type)
+ end
+
+ let(:connection) { validation.connection }
+
+ subject { described_class.new(validation) }
+
+ it 'validates the constraint while controlling statement timeout' do
+ allow(connection).to receive(:execute).and_call_original
+ expect(connection).to receive(:execute)
+ .with("SET statement_timeout TO '43200s'").ordered.and_call_original
+ expect(connection).to receive(:execute)
+ .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/).ordered.and_call_original
+ expect(connection).to receive(:execute)
+ .with("RESET statement_timeout").ordered.and_call_original
+
+ subject.perform
+ end
+
+ it 'removes the constraint validation record from table' do
+ expect(validation).to receive(:destroy!).and_call_original
+
+ expect { subject.perform }.to change { constraints_model.count }.by(-1)
+ end
+
+ it 'skips logic if not able to acquire exclusive lease' do
+ expect(lease).to receive(:try_obtain).ordered.and_return(false)
+ expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
+ expect(validation).not_to receive(:destroy!)
+
+ expect { subject.perform }.not_to change { constraints_model.count }
+ end
+
+ it 'logs messages around execution' do
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Starting to validate constraint'))
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Finished validating constraint'))
+ end
+
+ context 'when the constraint does not exist' do
+ before do
+ connection.create_table(table_name, force: true)
+ end
+
+ it 'skips validation and removes the record' do
+ expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
+
+ expect { subject.perform }.to change { constraints_model.count }.by(-1)
+ end
+
+ it 'logs an appropriate message' do
+ expected_message = /Skipping #{constraint_name} validation since it does not exist/
+
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: expected_message))
+ end
+ end
+
+ context 'with error handling' do
+ before do
+ allow(connection).to receive(:execute).and_call_original
+
+ allow(connection).to receive(:execute)
+ .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/)
+ .and_raise(ActiveRecord::StatementInvalid)
+ end
+
+ context 'on production' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
+
+ it 'increases execution attempts' do
+ expect { subject.perform }.to change { validation.attempts }.by(1)
+
+ expect(validation.last_error).to be_present
+ expect(validation).not_to be_destroyed
+ end
+
+ it 'logs an error message including the constraint_name' do
+ expect(Gitlab::AppLogger)
+ .to receive(:error)
+ .with(a_hash_including(:message, :constraint_name))
+ .and_call_original
+
+ subject.perform
+ end
+ end
+
+ context 'on development' do
+ it 'also raises errors' do
+ expect { subject.perform }
+ .to raise_error(ActiveRecord::StatementInvalid)
+ .and change { validation.attempts }.by(1)
+
+ expect(validation.last_error).to be_present
+ expect(validation).not_to be_destroyed
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb
new file mode 100644
index 00000000000..6f0cede7130
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "index validators" do |validator, expected_result|
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:database_indexes) do
+ [
+ ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'],
+ ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'],
+ ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']
+ ]
+ end
+
+ let(:inconsistency_type) { validator.name.demodulize.underscore }
+
+ let(:database_name) { 'main' }
+
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+
+ let(:connection) { database_model.connection }
+
+ let(:schema) { connection.current_schema }
+
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
+
+ subject(:result) { validator.new(structure_file, database).execute }
+
+ before do
+ allow(connection).to receive(:select_rows).and_return(database_indexes)
+ end
+
+ it 'returns index inconsistencies' do
+ expect(result.map(&:object_name)).to match_array(expected_result)
+ expect(result.map(&:type)).to all(eql inconsistency_type)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb
new file mode 100644
index 00000000000..ec7a881f7ce
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "schema objects assertions for" do |stmt_name|
+ let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt }
+ let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) }
+
+ describe '#name' do
+ it 'returns schema object name' do
+ expect(schema_object.name).to eq(name)
+ end
+ end
+
+ describe '#statement' do
+ it 'returns schema object statement' do
+ expect(schema_object.statement).to eq(statement)
+ end
+ end
+
+ describe '#table_name' do
+ it 'returns schema object table_name' do
+ expect(schema_object.table_name).to eq(table_name)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb
new file mode 100644
index 00000000000..96e58294675
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "table validators" do |validator, expected_result|
+ subject(:result) { validator.new(structure_file, database).execute }
+
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:inconsistency_type) { validator.name.demodulize.underscore }
+ let(:database_model) { Gitlab::Database.database_base_models['main'] }
+ let(:connection) { database_model.connection }
+ let(:schema) { connection.current_schema }
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
+ let(:database_tables) do
+ [
+ {
+ 'table_name' => 'wrong_table',
+ 'column_name' => 'id',
+ 'not_null' => true,
+ 'data_type' => 'integer',
+ 'column_default' => "nextval('audit_events_id_seq'::regclass)"
+ },
+ {
+ 'table_name' => 'wrong_table',
+ 'column_name' => 'description',
+ 'not_null' => true,
+ 'data_type' => 'character varying',
+ 'column_default' => nil
+ },
+ {
+ 'table_name' => 'extra_table',
+ 'column_name' => 'id',
+ 'not_null' => true,
+ 'data_type' => 'integer',
+ 'column_default' => "nextval('audit_events_id_seq'::regclass)"
+ },
+ {
+ 'table_name' => 'extra_table',
+ 'column_name' => 'email',
+ 'not_null' => true,
+ 'data_type' => 'character varying',
+ 'column_default' => nil
+ },
+ {
+ 'table_name' => 'extra_table_columns',
+ 'column_name' => 'id',
+ 'not_null' => true,
+ 'data_type' => 'bigint',
+ 'column_default' => "nextval('audit_events_id_seq'::regclass)"
+ },
+ {
+ 'table_name' => 'extra_table_columns',
+ 'column_name' => 'name',
+ 'not_null' => true,
+ 'data_type' => 'character varying(255)',
+ 'column_default' => nil
+ },
+ {
+ 'table_name' => 'extra_table_columns',
+ 'column_name' => 'extra_column',
+ 'not_null' => true,
+ 'data_type' => 'character varying(255)',
+ 'column_default' => nil
+ },
+ {
+ 'table_name' => 'missing_table_columns',
+ 'column_name' => 'id',
+ 'not_null' => true,
+ 'data_type' => 'bigint',
+ 'column_default' => 'NOT NULL'
+ }
+ ]
+ end
+
+ before do
+ allow(connection).to receive(:exec_query).and_return(database_tables)
+ end
+
+ it 'returns table inconsistencies' do
+ expect(result.map(&:object_name)).to match_array(expected_result)
+ expect(result.map(&:type)).to all(eql inconsistency_type)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb
new file mode 100644
index 00000000000..13a112275c2
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'trigger validators' do |validator, expected_result|
+ subject(:result) { validator.new(structure_file, database).execute }
+
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
+ let(:inconsistency_type) { validator.name.demodulize.underscore }
+ let(:database_name) { 'main' }
+ let(:schema) { 'public' }
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:connection) { database_model.connection }
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+
+ let(:database_triggers) do
+ [
+ ['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'],
+ ['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'],
+ ['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()']
+ ]
+ end
+
+ before do
+ allow(connection).to receive(:select_rows).and_return(database_triggers)
+ end
+
+ it 'returns trigger inconsistencies' do
+ expect(result.map(&:object_name)).to match_array(expected_result)
+ expect(result.map(&:type)).to all(eql inconsistency_type)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb
index f26b9a4a7bd..d388abb16c6 100644
--- a/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
def raw_repo_without_container(repository)
- Gitlab::Git::Repository.new(repository.shard,
- "#{repository.disk_path}.git",
- repository.repo_type.identifier_for_container(repository.container),
- repository.container.full_path)
+ Gitlab::Git::Repository.new(
+ repository.shard,
+ "#{repository.disk_path}.git",
+ repository.repo_type.identifier_for_container(repository.container),
+ repository.container.full_path
+ )
end
RSpec.shared_examples 'Gitaly feature flag actors are inferred from repository' do
diff --git a/spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb
new file mode 100644
index 00000000000..8a5e8397c3d
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a json logger' do |extra_params|
+ let(:now) { Time.now }
+ let(:correlation_id) { Labkit::Correlation::CorrelationId.current_id }
+
+ it 'formats strings' do
+ output = subject.format_message('INFO', now, 'test', 'Hello world')
+ data = Gitlab::Json.parse(output)
+
+ expect(data['severity']).to eq('INFO')
+ expect(data['time']).to eq(now.utc.iso8601(3))
+ expect(data['message']).to eq('Hello world')
+ expect(data['correlation_id']).to eq(correlation_id)
+ expect(data).to include(extra_params)
+ end
+
+ it 'formats hashes' do
+ output = subject.format_message('INFO', now, 'test', { hello: 1 })
+ data = Gitlab::Json.parse(output)
+
+ expect(data['severity']).to eq('INFO')
+ expect(data['time']).to eq(now.utc.iso8601(3))
+ expect(data['hello']).to eq(1)
+ expect(data['message']).to be_nil
+ expect(data['correlation_id']).to eq(correlation_id)
+ expect(data).to include(extra_params)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb
index 27ca27a9035..4b0e3234750 100644
--- a/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb
@@ -8,9 +8,9 @@ RSpec.shared_examples 'local and remote storage migration' do
where(:start_store, :end_store, :method) do
ObjectStorage::Store::LOCAL | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage
- ObjectStorage::Store::REMOTE | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ ObjectStorage::Store::REMOTE | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage
ObjectStorage::Store::REMOTE | ObjectStorage::Store::LOCAL | :migrate_to_local_storage
- ObjectStorage::Store::LOCAL | ObjectStorage::Store::LOCAL | :migrate_to_local_storage # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ ObjectStorage::Store::LOCAL | ObjectStorage::Store::LOCAL | :migrate_to_local_storage
end
with_them do
diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
index f83fecee4ea..0016f1e670d 100644
--- a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
@@ -38,8 +38,7 @@ RSpec.shared_examples 'access restricted confidential issues' do
let(:user) { author }
it 'lists project confidential issues' do
- expect(objects).to contain_exactly(issue,
- security_issue_1)
+ expect(objects).to contain_exactly(issue, security_issue_1)
expect(results.limited_issues_count).to eq 2
end
end
@@ -48,8 +47,7 @@ RSpec.shared_examples 'access restricted confidential issues' do
let(:user) { assignee }
it 'lists project confidential issues for assignee' do
- expect(objects).to contain_exactly(issue,
- security_issue_2)
+ expect(objects).to contain_exactly(issue, security_issue_2)
expect(results.limited_issues_count).to eq 2
end
end
@@ -60,9 +58,7 @@ RSpec.shared_examples 'access restricted confidential issues' do
end
it 'lists project confidential issues' do
- expect(objects).to contain_exactly(issue,
- security_issue_1,
- security_issue_2)
+ expect(objects).to contain_exactly(issue, security_issue_1, security_issue_2)
expect(results.limited_issues_count).to eq 3
end
end
@@ -72,9 +68,7 @@ RSpec.shared_examples 'access restricted confidential issues' do
context 'when admin mode is enabled', :enable_admin_mode do
it 'lists all project issues' do
- expect(objects).to contain_exactly(issue,
- security_issue_1,
- security_issue_2)
+ expect(objects).to contain_exactly(issue, security_issue_1, security_issue_2)
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
index 025f0d5c7ea..c2898513424 100644
--- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'a repo type' do
describe '#repository_for' do
it 'finds the repository for the repo type' do
- expect(described_class.repository_for(expected_container)).to eq(expected_repository)
+ expect(described_class.repository_for(expected_repository_resolver)).to eq(expected_repository)
end
it 'returns nil when container is nil' do
diff --git a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb
index a3e4379f4d3..18545698c27 100644
--- a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb
@@ -26,29 +26,4 @@ RSpec.shared_examples 'search results filtered by language' do
expect(blob_results.size).to eq(5)
expect(paths).to match_array(expected_paths)
end
-
- context 'when the search_blobs_language_aggregation feature flag is disabled' do
- before do
- stub_feature_flags(search_blobs_language_aggregation: false)
- end
-
- it 'does not filter by language', :sidekiq_inline, :aggregate_failures do
- expected_paths = %w[
- CHANGELOG
- CONTRIBUTING.md
- bar/branch-test.txt
- custom-highlighting/test.gitlab-custom
- files/ruby/popen.rb
- files/ruby/regex.rb
- files/ruby/version_info.rb
- files/whitespace
- encoding/test.txt
- files/markdown/ruby-style-guide.md
- ]
-
- paths = blob_results.map { |blob| blob.binary_path }
- expect(blob_results.size).to eq(10)
- expect(paths).to match_array(expected_paths)
- end
- end
end
diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
index ff03051ed37..74570a4da5c 100644
--- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, duplicate_key_ttl: Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_DUPLICATE_KEY_TTL)
end
- let(:expected_message) { "dropped #{strategy_name.to_s.humanize.downcase}" }
+ let(:humanized_strategy_name) { strategy_name.to_s.humanize.downcase }
subject(:strategy) { Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies.for(strategy_name).new(fake_duplicate_job) }
@@ -155,7 +155,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger)
expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger)
- expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {})
+ expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), humanized_strategy_name, {})
strategy.schedule({ 'jid' => 'new jid' }) {}
end
@@ -165,7 +165,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger)
allow(fake_duplicate_job).to receive(:options).and_return({ foo: :bar })
- expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar })
+ expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), humanized_strategy_name, { foo: :bar })
strategy.schedule({ 'jid' => 'new jid' }) {}
end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
index d4802a19202..169fceced7a 100644
--- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events for given event params' do
+RSpec.shared_examples 'tracked issuable snowplow and service ping events for given event params' do
before do
stub_application_setting(usage_ping_enabled: true)
end
- def count_unique(date_from: 1.minute.ago, date_to: 1.minute.from_now)
+ def count_unique(date_from: Date.today.beginning_of_week, date_to: 1.week.from_now)
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
end
@@ -27,35 +27,23 @@ RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events
expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params))
end
-
- context 'with route_hll_to_snowplow_phase2 disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'does not emit snowplow event' do
- track_action({ author: user1 }.merge(track_params))
-
- expect_no_snowplow_event
- end
- end
end
-RSpec.shared_examples 'daily tracked issuable snowplow and service ping events with project' do
- it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do
+RSpec.shared_examples 'tracked issuable snowplow and service ping events with project' do
+ it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do
let(:context) do
Gitlab::Tracking::ServicePingContext
.new(data_source: :redis_hll, event: event_property)
.to_h
end
- let(:track_params) { { project: project } }
- let(:event_params) { track_params.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) }
+ let(:track_params) { original_params || { project: project } }
+ let(:event_params) { { project: project }.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) }
end
end
-RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events with namespace' do
- it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do
+RSpec.shared_examples 'tracked issuable snowplow and service ping events with namespace' do
+ it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do
let(:context) do
Gitlab::Tracking::ServicePingContext
.new(data_source: :redis_hll, event: event_property)
diff --git a/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb
new file mode 100644
index 00000000000..a42d1450e4d
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do
+ let(:randomhex) { 'randomhex' }
+
+ it 'check email domain' do
+ expect(subject.email).to end_with("@#{email_domain}")
+ end
+
+ it 'contains SecureRandom part' do
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+
+ expect(subject.username).to include("_#{randomhex}")
+ expect(subject.email).to include("_#{randomhex}@")
+ end
+
+ it 'email name is the same as username' do
+ expect(subject.email).to include("#{subject.username}@")
+ end
+
+ context 'when conflicts' do
+ let(:reserved_username) { "#{username_prefix}_#{randomhex}" }
+ let(:reserved_email) { "#{reserved_username}@#{email_domain}" }
+
+ shared_examples 'uniquifies username and email' do
+ it 'uniquifies username and email' do
+ expect(subject.username).to eq("#{reserved_username}1")
+ expect(subject.email).to include("#{subject.username}@")
+ end
+ end
+
+ context 'when username is reserved' do
+ context 'when username is reserved by user' do
+ before do
+ create(:user, username: reserved_username)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with top-level group namespace' do
+ before do
+ create(:group, path: reserved_username)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with top-level group namespace that includes upcased characters' do
+ before do
+ create(:group, path: reserved_username.upcase)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+ end
+
+ context 'when email is reserved' do
+ context 'when it conflicts with confirmed primary email' do
+ before do
+ create(:user, email: reserved_email)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with unconfirmed primary email' do
+ before do
+ create(:user, :unconfirmed, email: reserved_email)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with confirmed secondary email' do
+ before do
+ create(:email, :confirmed, email: reserved_email)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+ end
+
+ context 'when email and username is reserved' do
+ before do
+ create(:user, email: reserved_email)
+ create(:user, username: "#{reserved_username}1")
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ it 'uniquifies username and email' do
+ expect(subject.username).to eq("#{reserved_username}2")
+
+ expect(subject.email).to include("#{subject.username}@")
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/menus_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb
index 2c2cb362b07..0aa98517444 100644
--- a/spec/support/shared_examples/lib/menus_shared_examples.rb
+++ b/spec/support/shared_examples/lib/menus_shared_examples.rb
@@ -37,3 +37,58 @@ RSpec.shared_examples_for 'pill_count formatted results' do
expect(pill_count).to eq('112.6k')
end
end
+
+RSpec.shared_examples_for 'serializable as super_sidebar_menu_args' do
+ let(:extra_attrs) { raise NotImplementedError }
+
+ it 'returns hash with provided attributes' do
+ expect(menu.serialize_as_menu_item_args).to eq({
+ title: menu.title,
+ link: menu.link,
+ active_routes: menu.active_routes,
+ container_html_options: menu.container_html_options,
+ **extra_attrs
+ })
+ end
+
+ it 'returns hash with an item_id' do
+ expect(menu.serialize_as_menu_item_args[:item_id]).not_to be_nil
+ end
+end
+
+RSpec.shared_examples_for 'not serializable as super_sidebar_menu_args' do
+ it 'returns nil' do
+ expect(menu.serialize_as_menu_item_args).to be_nil
+ end
+end
+
+RSpec.shared_examples_for 'a panel with uniquely identifiable menu items' do
+ let(:menu_items) do
+ subject.instance_variable_get(:@menus)
+ .flat_map { |menu| menu.instance_variable_get(:@items) }
+ end
+
+ it 'all menu_items have unique item_id' do
+ duplicated_ids = menu_items.group_by(&:item_id).reject { |_, v| (v.size < 2) }
+
+ expect(duplicated_ids).to eq({})
+ end
+
+ it 'all menu_items have an item_id' do
+ items_with_nil_id = menu_items.select { |item| item.item_id.nil? }
+
+ expect(items_with_nil_id).to match_array([])
+ end
+end
+
+RSpec.shared_examples_for 'a panel with all menu_items categorized' do
+ let(:uncategorized_menu) do
+ subject.instance_variable_get(:@menus)
+ .find { |menu| menu.instance_of?(::Sidebars::UncategorizedMenu) }
+ end
+
+ it 'has no uncategorized menu_items' do
+ uncategorized_menu_items = uncategorized_menu.instance_variable_get(:@items)
+ expect(uncategorized_menu_items).to eq([])
+ end
+end
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index e0b411e1e2a..fa3e9bf5340 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -90,7 +90,9 @@ RSpec.shared_examples 'Sentry API response size limit' do
end
it 'raises an exception when response is too large' do
- expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
- 'Sentry API response is too big. Limit is 1 MB.')
+ expect { subject }.to raise_error(
+ ErrorTracking::SentryClient::ResponseInvalidSizeError,
+ 'Sentry API response is too big. Limit is 1 MB.'
+ )
end
end
diff --git a/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb
new file mode 100644
index 00000000000..f913c6b8a9e
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Admin menu' do |link:, title:, icon:, separated: false|
+ let_it_be(:user) { build(:user, :admin) }
+
+ before do
+ allow(user).to receive(:can_admin_all_resources?).and_return(true)
+ end
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'renders the correct link' do
+ expect(subject.link).to match link
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq title
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to be icon
+ end
+
+ it 'renders the separator if needed' do
+ expect(subject.separated?).to be separated
+ end
+
+ describe '#render?' do
+ context 'when user is admin' do
+ it 'renders' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when user is not admin' do
+ it 'does not render' do
+ expect(described_class.new(Sidebars::Context.new(current_user: build(:user),
+ container: nil)).render?).to be false
+ end
+ end
+
+ context 'when user is not logged in' do
+ it 'does not render' do
+ expect(described_class.new(Sidebars::Context.new(current_user: nil, container: nil)).render?).to be false
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Admin menu without sub menus' do |active_routes:|
+ let_it_be(:user) { build(:user, :admin) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu(s)' do
+ expect(subject.has_items?).to be false
+ end
+
+ it 'defines correct active route' do
+ expect(subject.active_routes).to eq active_routes
+ end
+end
+
+RSpec.shared_examples 'Admin menu with sub menus' do
+ let_it_be(:user) { build(:user, :admin) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'contains submemus' do
+ expect(subject.has_items?).to be true
+ end
+end
diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb
new file mode 100644
index 00000000000..5e8aebb4f29
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'User profile menu' do |title:, icon:, active_route:|
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:user) { build(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.has_items?).to be false
+ end
+
+ it 'renders the correct link' do
+ expect(subject.link).to match link
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq title
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to eq icon
+ end
+
+ it 'defines correct active route' do
+ expect(subject.active_routes[:path]).to be active_route
+ end
+
+ it 'renders if user is logged in' do
+ expect(subject.render?).to be true
+ end
+
+ [:blocked, :banned].each do |trait|
+ context "when viewed user is #{trait}" do
+ let_it_be(:viewed_user) { build(:user, trait) }
+ let(:context) { Sidebars::Context.new(current_user: user, container: viewed_user) }
+
+ context 'when user is not logged in' do
+ it 'is not allowed to view the menu item' do
+ expect(described_class.new(Sidebars::Context.new(current_user: nil,
+ container: viewed_user)).render?).to be false
+ end
+ end
+
+ context 'when current user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_user_profile, viewed_user).and_return(true)
+ end
+
+ it 'is allowed to view the menu item' do
+ expect(described_class.new(context).render?).to be true
+ end
+ end
+
+ context 'when current user does not have permission' do
+ it 'is not allowed to view the menu item' do
+ expect(described_class.new(context).render?).to be false
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Followers/followees counts' do |symbol|
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:user) { build(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ context 'when there are items' do
+ before do
+ allow(user).to receive(symbol).and_return([1, 2])
+ end
+
+ it 'renders the pill' do
+ expect(subject.has_pill?).to be(true)
+ end
+
+ it 'returns the count' do
+ expect(subject.pill_count).to be(2)
+ end
+ end
+
+ context 'when there are no items' do
+ it 'does not render the pill' do
+ expect(subject.has_pill?).to be(false)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb
new file mode 100644
index 00000000000..b91386d1935
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'User settings menu' do |link:, title:, icon:, active_routes:|
+ let_it_be(:user) { create(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.has_items?).to be false
+ end
+
+ it 'renders the correct link' do
+ expect(subject.link).to match link
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq title
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to be icon
+ end
+
+ it 'defines correct active route' do
+ expect(subject.active_routes).to eq active_routes
+ end
+end
+
+RSpec.shared_examples 'User settings menu #render? method' do
+ describe '#render?' do
+ subject { described_class.new(context) }
+
+ context 'when user is logged in' do
+ let_it_be(:user) { build(:user) }
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ it 'renders' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/mailers/export_csv_shared_examples.rb b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb
new file mode 100644
index 00000000000..731d7c810f9
--- /dev/null
+++ b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'export csv email' do |collection_type|
+ include_context 'gitlab email notification'
+
+ it 'attachment has csv mime type' do
+ expect(attachment.mime_type).to eq 'text/csv'
+ end
+
+ it 'generates a useful filename' do
+ expect(attachment.filename).to include(Date.today.year.to_s)
+ expect(attachment.filename).to include(collection_type)
+ expect(attachment.filename).to include('myproject')
+ expect(attachment.filename).to end_with('.csv')
+ end
+
+ it 'mentions number of objects and project name' do
+ expect(subject).to have_content '3'
+ expect(subject).to have_content empty_project.name
+ end
+
+ it "doesn't need to mention truncation by default" do
+ expect(subject).not_to have_content 'truncated'
+ end
+
+ context 'when truncated' do
+ let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
+
+ it 'mentions that the csv has been truncated' do
+ expect(subject).to have_content 'truncated'
+ end
+
+ it 'mentions the number of objects written and expected' do
+ expect(subject).to have_content "10 of 12 #{collection_type.humanize.downcase}"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index 2e182fb399d..cf1ab7697ab 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -59,7 +59,7 @@ end
RSpec.shared_examples 'an email with X-GitLab headers containing project details' do
it 'has X-GitLab-Project headers' do
aggregate_failures do
- full_path_as_domain = "#{project.name}.#{project.namespace.path}"
+ full_path_as_domain = "#{project.path}.#{project.namespace.path}"
is_expected.to have_header('X-GitLab-Project', /#{project.name}/)
is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/)
is_expected.to have_header('X-GitLab-Project-Path', /#{project.full_path}/)
@@ -294,3 +294,17 @@ RSpec.shared_examples 'does not render a manage notifications link' do
end
end
end
+
+RSpec.shared_examples 'email with default notification reason' do
+ it do
+ is_expected.to have_body_text("You're receiving this email because of your account")
+ is_expected.to have_plain_text_content("You're receiving this email because of your account")
+ end
+end
+
+RSpec.shared_examples 'email with link to issue' do
+ it do
+ is_expected.to have_body_text(%(<a href="#{project_issue_url(project, issue)}">view it on GitLab</a>))
+ is_expected.to have_plain_text_content("view it on GitLab: #{project_issue_url(project, issue)}")
+ end
+end
diff --git a/spec/support/shared_examples/metrics_instrumentation_shared_examples.rb b/spec/support/shared_examples/metrics_instrumentation_shared_examples.rb
new file mode 100644
index 00000000000..cef9860fe25
--- /dev/null
+++ b/spec/support/shared_examples/metrics_instrumentation_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a correct instrumented metric value' do |params|
+ let(:time_frame) { params[:time_frame] }
+ let(:options) { params[:options] }
+ let(:metric) { described_class.new(time_frame: time_frame, options: options) }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ if metric.respond_to?(:relation, true) && metric.send(:relation).respond_to?(:connection)
+ allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false)
+ end
+ end
+
+ it 'has correct value' do
+ expect(metric.value).to eq(expected_value)
+ end
+end
+
+RSpec.shared_examples 'a correct instrumented metric query' do |params|
+ let(:time_frame) { params[:time_frame] }
+ let(:options) { params[:options] }
+ let(:metric) { described_class.new(time_frame: time_frame, options: options) }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'has correct generate query' do
+ expect(metric.to_sql).to eq(expected_query)
+ end
+end
+
+RSpec.shared_examples 'a correct instrumented metric value and query' do |params|
+ it_behaves_like 'a correct instrumented metric value', params
+ it_behaves_like 'a correct instrumented metric query', params
+end
diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
new file mode 100644
index 00000000000..28eac52256f
--- /dev/null
+++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'migration that adds widget to work items definitions' do |widget_name:|
+ let(:migration) { described_class.new }
+ let(:work_item_definitions) { table(:work_item_widget_definitions) }
+
+ describe '#up' do
+ it "creates widget definition in all types" do
+ work_item_definitions.where(name: widget_name).delete_all
+
+ expect { migrate! }.to change { work_item_definitions.count }.by(7)
+ expect(work_item_definitions.all.pluck(:name)).to include(widget_name)
+ end
+
+ it 'logs a warning if the type is missing' do
+ allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id).and_call_original
+ allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id)
+ .with('Issue', nil).and_return(nil)
+
+ expect(Gitlab::AppLogger).to receive(:warn).with('type Issue is missing, not adding widget')
+ migrate!
+ end
+ end
+
+ describe '#down' do
+ it "removes definitions for widget" do
+ migrate!
+
+ expect { migration.down }.to change { work_item_definitions.count }.by(-7)
+ expect(work_item_definitions.all.pluck(:name)).not_to include(widget_name)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb
index 3d765b6ca93..10f3263d4fc 100644
--- a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb
+++ b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb
@@ -10,3 +10,13 @@ RSpec.shared_examples 'having unique enum values' do
end
end
end
+
+RSpec.shared_examples 'having enum with nil value' do
+ it 'has enum with nil value' do
+ subject.public_send("#{attr_value}!")
+
+ expect(subject.public_send("#{attr}_for_database")).to be_nil
+ expect(subject.public_send("#{attr}?")).to eq(true)
+ expect(subject.class.public_send(attr_value)).to eq([subject])
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index 085fec6ff1e..addd37cde32 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -221,11 +221,13 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with commit comment" do
let_it_be(:note) do
- create(:note_on_commit,
- author: user,
- project: project,
- commit_id: project.repository.commit.id,
- note: "a comment on a commit")
+ create(
+ :note_on_commit,
+ author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: "a comment on a commit"
+ )
end
it_behaves_like "triggered #{integration_name} integration"
@@ -261,9 +263,11 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with failed pipeline" do
let_it_be(:pipeline) do
- create(:ci_pipeline,
- project: project, status: "failed",
- sha: project.commit.sha, ref: project.default_branch)
+ create(
+ :ci_pipeline,
+ project: project, status: "failed",
+ sha: project.commit.sha, ref: project.default_branch
+ )
end
it_behaves_like "triggered #{integration_name} integration"
@@ -271,9 +275,11 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with succeeded pipeline" do
let_it_be(:pipeline) do
- create(:ci_pipeline,
- project: project, status: "success",
- sha: project.commit.sha, ref: project.default_branch)
+ create(
+ :ci_pipeline,
+ project: project, status: "success",
+ sha: project.commit.sha, ref: project.default_branch
+ )
end
context "with default notify_only_broken_pipelines" do
diff --git a/spec/support/shared_examples/models/ci/token_format_shared_examples.rb b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
new file mode 100644
index 00000000000..0272982e2d0
--- /dev/null
+++ b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'ensures runners_token is prefixed' do |factory|
+ subject(:record) { FactoryBot.build(factory) }
+
+ describe '#runners_token', feature_category: :system_access do
+ let(:runners_prefix) { RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX }
+
+ it 'generates runners_token which starts with runner prefix' do
+ expect(record.runners_token).to match(a_string_starting_with(runners_prefix))
+ end
+
+ context 'when record has an invalid token' do
+ subject(:record) { FactoryBot.build(factory, runners_token: invalid_runners_token) }
+
+ let(:invalid_runners_token) { "not_start_with_runners_prefix" }
+
+ it 'generates runners_token which starts with runner prefix' do
+ expect(record.runners_token).to match(a_string_starting_with(runners_prefix))
+ end
+
+ it 'changes the attribute values for runners_token and runners_token_encrypted' do
+ expect { record.runners_token }
+ .to change { record[:runners_token] }.from(invalid_runners_token).to(nil)
+ .and change { record[:runners_token_encrypted] }.from(nil)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb b/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb
index 8d6dcfef925..140968da272 100644
--- a/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb
+++ b/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb
@@ -41,10 +41,12 @@ RSpec.shared_examples '#prometheus_client shared' do
subject.cluster.platform_kubernetes.namespace = 'a-namespace'
stub_kubeclient_discover(cluster.platform_kubernetes.api_url)
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
+ create(
+ :cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project
+ )
end
it 'creates proxy prometheus_client' do
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
index 122774a9028..a196b63585c 100644
--- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -17,8 +17,12 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
[4, 1.second.from_now], # Exceeded the grace period, set by #backoff!
[4, Time.current] # Exceeded the grace period, set by #backoff!, edge-case
].map do |(recent_failures, disabled_until)|
- create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
-disabled_until: disabled_until)
+ create(
+ hook_factory,
+ **default_factory_arguments,
+ recent_failures: recent_failures,
+ disabled_until: disabled_until
+ )
end
end
@@ -45,8 +49,12 @@ disabled_until: disabled_until)
[0, suspended],
[0, expired]
].map do |(recent_failures, disabled_until)|
- create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
-disabled_until: disabled_until)
+ create(
+ hook_factory,
+ **default_factory_arguments,
+ recent_failures: recent_failures,
+ disabled_until: disabled_until
+ )
end
end
@@ -61,6 +69,20 @@ disabled_until: disabled_until)
# Nothing is missing
expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'causes all hooks to be considered executable' do
+ expect(find_hooks.executable.count).to eq(16)
+ end
+
+ it 'causes no hooks to be considered disabled' do
+ expect(find_hooks.disabled).to be_empty
+ end
+ end
end
describe '#executable?', :freeze_time do
@@ -108,6 +130,16 @@ disabled_until: disabled_until)
it 'has the correct state' do
expect(web_hook.executable?).to eq(executable)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is always executable' do
+ expect(web_hook).to be_executable
+ end
+ end
end
end
@@ -151,7 +183,7 @@ disabled_until: disabled_until)
context 'when we have exhausted the grace period' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
context 'when the hook is permanently disabled' do
@@ -172,6 +204,16 @@ disabled_until: disabled_until)
def run_expectation
expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not increment backoff count' do
+ expect { hook.failed! }.not_to change { hook.backoff_count }
+ end
+ end
end
end
end
@@ -181,6 +223,16 @@ disabled_until: disabled_until)
def run_expectation
expect { hook.failed! }.to change { hook.recent_failures }.by(1)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not increment recent failure count' do
+ expect { hook.failed! }.not_to change { hook.recent_failures }
+ end
+ end
end
end
@@ -189,6 +241,16 @@ disabled_until: disabled_until)
expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
end
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not disable the hook' do
+ expect { hook.disable! }.not_to change { hook.executable? }
+ end
+ end
+
it 'does nothing if the hook is already disabled' do
allow(hook).to receive(:permanently_disabled?).and_return(true)
@@ -210,7 +272,7 @@ disabled_until: disabled_until)
end
it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
- WebHook::FAILURE_THRESHOLD.times do
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do
hook.backoff!
expect(hook).not_to be_temporarily_disabled
end
@@ -221,13 +283,23 @@ disabled_until: disabled_until)
context 'when hook has been told to back off' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
hook.backoff!
end
it 'is true' do
expect(hook).to be_temporarily_disabled
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is false' do
+ expect(hook).not_to be_temporarily_disabled
+ end
+ end
end
end
@@ -244,6 +316,16 @@ disabled_until: disabled_until)
it 'is true' do
expect(hook).to be_permanently_disabled
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is false' do
+ expect(hook).not_to be_permanently_disabled
+ end
+ end
end
end
@@ -258,15 +340,31 @@ disabled_until: disabled_until)
end
it { is_expected.to eq :disabled }
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it { is_expected.to eq(:executable) }
+ end
end
context 'when hook has been backed off' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
hook.disabled_until = 1.hour.from_now
end
it { is_expected.to eq :temporarily_disabled }
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it { is_expected.to eq(:executable) }
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
index a4db4e25db3..c51e4999e81 100644
--- a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
@@ -112,9 +112,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
it 'does not allow the local value to be saved' do
subgroup_settings.send("#{settings_attribute_name}=", nil)
- expect { subgroup_settings.save! }
- .to raise_error(ActiveRecord::RecordInvalid,
- /cannot be changed because it is locked by an ancestor/)
+ expect { subgroup_settings.save! }.to raise_error(
+ ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/
+ )
end
end
@@ -171,15 +172,59 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
end
describe "##{settings_attribute_name}=" do
- before do
- subgroup_settings.update!(settings_attribute_name => nil)
- group_settings.update!(settings_attribute_name => true)
+ using RSpec::Parameterized::TableSyntax
+
+ where(:parent_value, :current_subgroup_value, :new_subgroup_value, :expected_subgroup_value_after_update) do
+ true | nil | true | nil
+ true | nil | "true" | nil
+ true | false | true | true
+ true | false | "true" | true
+ true | true | false | false
+ true | true | "false" | false
+ false | nil | false | nil
+ false | nil | true | true
+ false | true | false | false
+ false | false | true | true
end
- it 'does not save the value locally when it matches the cascaded value' do
- subgroup_settings.update!(settings_attribute_name => true)
+ with_them do
+ before do
+ subgroup_settings.update!(settings_attribute_name => current_subgroup_value)
+ group_settings.update!(settings_attribute_name => parent_value)
+ end
- expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(nil)
+ it 'validates starting values from before block', :aggregate_failures do
+ expect(group_settings.reload.read_attribute(settings_attribute_name)).to eq(parent_value)
+ expect(subgroup_settings.reload.read_attribute(settings_attribute_name)).to eq(current_subgroup_value)
+ end
+
+ it 'does not save the value locally when it matches cascaded value', :aggregate_failures do
+ subgroup_settings.send("#{settings_attribute_name}=", new_subgroup_value)
+
+ # Verify dirty value
+ expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(expected_subgroup_value_after_update)
+
+ subgroup_settings.save!
+
+ # Verify persisted value
+ expect(subgroup_settings.reload.read_attribute(settings_attribute_name))
+ .to eq(expected_subgroup_value_after_update)
+ end
+
+ context 'when mass assigned' do
+ before do
+ subgroup_settings.attributes =
+ { settings_attribute_name => new_subgroup_value, "lock_#{settings_attribute_name}" => false }
+ end
+
+ it 'does not save the value locally when it matches cascaded value', :aggregate_failures do
+ subgroup_settings.save!
+
+ # Verify persisted value
+ expect(subgroup_settings.reload.read_attribute(settings_attribute_name))
+ .to eq(expected_subgroup_value_after_update)
+ end
+ end
end
end
@@ -277,9 +322,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
it 'does not allow the attribute to be saved' do
subgroup_settings.send("lock_#{settings_attribute_name}=", true)
- expect { subgroup_settings.save! }
- .to raise_error(ActiveRecord::RecordInvalid,
- /cannot be changed because it is locked by an ancestor/)
+ expect { subgroup_settings.save! }.to raise_error(
+ ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/
+ )
end
end
@@ -299,9 +345,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
it 'does not allow the lock to be saved when the attribute is nil' do
subgroup_settings.send("#{settings_attribute_name}=", nil)
- expect { subgroup_settings.save! }
- .to raise_error(ActiveRecord::RecordInvalid,
- /cannot be nil when locking the attribute/)
+ expect { subgroup_settings.save! }.to raise_error(
+ ActiveRecord::RecordInvalid,
+ /cannot be nil when locking the attribute/
+ )
end
it 'copies the cascaded value when locking the attribute if the local value is nil', :aggregate_failures do
@@ -320,9 +367,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
it 'does not allow the attribute to be saved' do
subgroup_settings.send("lock_#{settings_attribute_name}=", true)
- expect { subgroup_settings.save! }
- .to raise_error(ActiveRecord::RecordInvalid,
- /cannot be changed because it is locked by an ancestor/)
+ expect { subgroup_settings.save! }.to raise_error(
+ ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/
+ )
end
end
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index 5755b9a56b1..9d189842b28 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -17,6 +17,11 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
let(:amount) { 10 }
let(:increment) { Gitlab::Counters::Increment.new(amount: amount, ref: 3) }
let(:counter_key) { model.counter(attribute).key }
+ let(:returns_current) do
+ model.class.counter_attributes
+ .find { |a| a[:attribute] == attribute }
+ .fetch(:returns_current, false)
+ end
subject { model.increment_counter(attribute, increment) }
@@ -61,6 +66,33 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
+ describe '#increment_amount' do
+ it 'increases the egress in cache' do
+ model.increment_amount(attribute, 3)
+
+ expect(model.counter(attribute).get).to eq(3)
+ end
+ end
+
+ describe '#current_counter' do
+ let(:data_transfer_node) do
+ args = { project: project }
+ args[attribute] = 2
+ create(:project_data_transfer, **args)
+ end
+
+ it 'increases the amount in cache' do
+ if returns_current
+ incremented_by = 4
+ db_state = model.read_attribute(attribute)
+
+ model.send("increment_#{attribute}".to_sym, incremented_by)
+
+ expect(model.send(attribute)).to eq(db_state + incremented_by)
+ end
+ end
+ end
+
context 'when increment amount is 0' do
let(:amount) { 0 }
@@ -155,14 +187,24 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
describe '#update_counters_with_lease' do
- let(:increments) { { build_artifacts_size: 1, packages_size: 2 } }
+ let_it_be(:first_attribute) { counter_attributes.first }
+ let_it_be(:second_attribute) { counter_attributes.second }
+
+ let_it_be(:increments) do
+ increments_hash = {}
+
+ increments_hash[first_attribute] = 1
+ increments_hash[second_attribute] = 2
+
+ increments_hash
+ end
subject { model.update_counters_with_lease(increments) }
it 'updates counters of the record' do
expect { subject }
- .to change { model.reload.build_artifacts_size }.by(1)
- .and change { model.reload.packages_size }.by(2)
+ .to change { model.reload.send(first_attribute) }.by(1)
+ .and change { model.reload.send(second_attribute) }.by(2)
end
it_behaves_like 'obtaining lease to update database' do
@@ -193,17 +235,4 @@ RSpec.shared_examples 'obtaining lease to update database' do
expect { subject }.not_to raise_error
end
end
-
- context 'when feature flag counter_attribute_db_lease_for_update is disabled' do
- before do
- stub_feature_flags(counter_attribute_db_lease_for_update: false)
- allow(model).to receive(:in_lock).and_call_original
- end
-
- it 'does not attempt to get a lock' do
- expect(model).not_to receive(:in_lock)
-
- subject
- end
- end
end
diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
index 2e528f7996c..2dad35dc46e 100644
--- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
@@ -35,7 +35,6 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.to_s }
let(:action) { 'perform_integrations_action' }
let(:namespace) { project.namespace }
diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index 0ef9ab25505..28d2d4f1597 100644
--- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -465,10 +465,13 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'when commit comment event executed' do
let(:commit_note) do
- create(:note_on_commit, author: user,
- project: project,
- commit_id: project.repository.commit.id,
- note: 'a comment on a commit')
+ create(
+ :note_on_commit,
+ author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit'
+ )
end
let(:data) do
@@ -480,8 +483,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'when merge request comment event executed' do
let(:merge_request_note) do
- create(:note_on_merge_request, project: project,
- note: 'a comment on a merge request')
+ create(:note_on_merge_request, project: project, note: 'a comment on a merge request')
end
let(:data) do
@@ -493,8 +495,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'when issue comment event executed' do
let(:issue_note) do
- create(:note_on_issue, project: project,
- note: 'a comment on an issue')
+ create(:note_on_issue, project: project, note: 'a comment on an issue')
end
let(:data) do
@@ -506,8 +507,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'when snippet comment event executed' do
let(:snippet_note) do
- create(:note_on_project_snippet, project: project,
- note: 'a comment on a snippet')
+ create(:note_on_project_snippet, project: project, note: 'a comment on a snippet')
end
let(:data) do
@@ -522,9 +522,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
let_it_be(:user) { create(:user) }
let_it_be_with_refind(:project) { create(:project, :repository, creator: user) }
let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: status,
- sha: project.commit.sha, ref: project.default_branch)
+ create(:ci_pipeline, project: project, status: status, sha: project.commit.sha, ref: project.default_branch)
end
before do
@@ -557,9 +555,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'with failed pipeline' do
context 'on default branch' do
let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: :failed,
- sha: project.commit.sha, ref: project.default_branch)
+ create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: project.default_branch)
end
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
@@ -587,9 +583,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: :failed,
- sha: project.commit.sha, ref: 'a-protected-branch')
+ create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: 'a-protected-branch')
end
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
@@ -617,9 +611,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: :failed,
- sha: project.commit.sha, ref: '1-stable')
+ create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: '1-stable')
end
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
@@ -643,9 +635,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'on a neither protected nor default branch' do
let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: :failed,
- sha: project.commit.sha, ref: 'a-random-branch')
+ create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: 'a-random-branch')
end
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
diff --git a/spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb
new file mode 100644
index 00000000000..dd27ff3844f
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'protected branch access' do
+ include_examples 'protected ref access', :protected_branch
+
+ it { is_expected.to belong_to(:protected_branch) }
+
+ describe '#project' do
+ before do
+ allow(protected_ref).to receive(:project)
+ end
+
+ it 'delegates project to protected_branch association' do
+ described_class.new(protected_branch: protected_ref).project
+
+ expect(protected_ref).to have_received(:project)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb
new file mode 100644
index 00000000000..8e15720c79a
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'protected ref access allowed_access_levels' do |excludes: []|
+ describe '::allowed_access_levels' do
+ subject { described_class.allowed_access_levels }
+
+ let(:all_levels) do
+ [
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::MAINTAINER,
+ Gitlab::Access::ADMIN,
+ Gitlab::Access::NO_ACCESS
+ ]
+ end
+
+ context 'when running on Gitlab.com?' do
+ let(:levels) { all_levels.excluding(Gitlab::Access::ADMIN, *excludes) }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it { is_expected.to match_array(levels) }
+ end
+
+ context 'when self hosted?' do
+ let(:levels) { all_levels.excluding(*excludes) }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it { is_expected.to match_array(levels) }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb
new file mode 100644
index 00000000000..4753d7a4556
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'protected ref access' do |association|
+ include ExternalAuthorizationServiceHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:protected_ref) { create(association, project: project) } # rubocop:disable Rails/SaveBang
+
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) }
+
+ it { is_expected.to validate_presence_of(:access_level) }
+
+ context 'when not role?' do
+ before do
+ allow(subject).to receive(:role?).and_return(false)
+ end
+
+ it { is_expected.not_to validate_presence_of(:access_level) }
+ end
+
+ describe '::human_access_levels' do
+ subject { described_class.human_access_levels }
+
+ let(:levels) do
+ {
+ Gitlab::Access::DEVELOPER => "Developers + Maintainers",
+ Gitlab::Access::MAINTAINER => "Maintainers",
+ Gitlab::Access::ADMIN => 'Instance admins',
+ Gitlab::Access::NO_ACCESS => "No one"
+ }.slice(*described_class.allowed_access_levels)
+ end
+
+ it { is_expected.to eq(levels) }
+ end
+
+ describe '#check_access' do
+ let_it_be(:current_user) { create(:user) }
+
+ let(:access_level) { ::Gitlab::Access::DEVELOPER }
+
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ subject do
+ described_class.new(
+ association => protected_ref,
+ access_level: access_level
+ )
+ end
+
+ context 'when current_user is nil' do
+ it { expect(subject.check_access(nil)).to eq(false) }
+ end
+
+ context 'when access_level is NO_ACCESS' do
+ let(:access_level) { ::Gitlab::Access::NO_ACCESS }
+
+ it { expect(subject.check_access(current_user)).to eq(false) }
+ end
+
+ context 'when instance admin access is configured' do
+ let(:access_level) { Gitlab::Access::ADMIN }
+
+ context 'when current_user is a maintainer' do
+ it { expect(subject.check_access(current_user)).to eq(false) }
+ end
+
+ context 'when current_user is admin' do
+ before do
+ allow(current_user).to receive(:admin?).and_return(true)
+ end
+
+ it { expect(subject.check_access(current_user)).to eq(true) }
+ end
+ end
+
+ context 'when current_user can push_code to project' do
+ context 'and member access is high enough' do
+ it { expect(subject.check_access(current_user)).to eq(true) }
+
+ context 'when external authorization denies access' do
+ before do
+ external_service_deny_access(current_user, project)
+ end
+
+ it { expect(subject.check_access(current_user)).to be_falsey }
+ end
+ end
+
+ context 'and member access is too low' do
+ let(:access_level) { ::Gitlab::Access::MAINTAINER }
+
+ it { expect(subject.check_access(current_user)).to eq(false) }
+ end
+ end
+
+ context 'when current_user cannot push_code to project' do
+ before do
+ allow(current_user).to receive(:can?).with(:push_code, project).and_return(false)
+ end
+
+ it { expect(subject.check_access(current_user)).to eq(false) }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb
new file mode 100644
index 00000000000..49f616d5a59
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'protected tag access' do
+ include_examples 'protected ref access', :protected_tag
+
+ let_it_be(:protected_tag) { create(:protected_tag) }
+
+ it { is_expected.to belong_to(:protected_tag) }
+
+ describe '#project' do
+ before do
+ allow(protected_tag).to receive(:project)
+ end
+
+ it 'delegates project to protected_tag association' do
+ described_class.new(protected_tag: protected_tag).project
+
+ expect(protected_tag).to have_received(:project)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index e4958779957..b04ac40b309 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -84,9 +84,12 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:max_date) { mid_point + 10.days }
def box(from, to)
- create(factory, *timebox_args,
- start_date: from || open_on_left,
- due_date: to || open_on_right)
+ create(
+ factory,
+ *timebox_args,
+ start_date: from || open_on_left,
+ due_date: to || open_on_right
+ )
end
it 'can find overlapping timeboxes' do
diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
index 848840ee297..f98528ffedc 100644
--- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
@@ -18,8 +18,12 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
[3, nil],
[3, 1.day.ago]
].map do |(recent_failures, disabled_until)|
- create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
-disabled_until: disabled_until)
+ create(
+ hook_factory,
+ **default_factory_arguments,
+ recent_failures: recent_failures,
+ disabled_until: disabled_until
+ )
end
end
@@ -110,7 +114,7 @@ disabled_until: disabled_until)
context 'when we have exhausted the grace period' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
it 'does not disable the hook' do
@@ -131,7 +135,7 @@ disabled_until: disabled_until)
expect(hook).not_to be_temporarily_disabled
# Backing off
- WebHook::FAILURE_THRESHOLD.times do
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do
hook.backoff!
expect(hook).not_to be_temporarily_disabled
end
@@ -167,7 +171,7 @@ disabled_until: disabled_until)
context 'when hook has been backed off' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
hook.disabled_until = 1.hour.from_now
end
diff --git a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
index cd6eb8c77fa..113dcc266fc 100644
--- a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
@@ -19,7 +19,7 @@ RSpec.shared_examples 'something that has web-hooks' do
context 'when there is a failed hook' do
before do
hook = create_hook
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
end
it { is_expected.to eq(true) }
@@ -83,7 +83,7 @@ RSpec.shared_examples 'something that has web-hooks' do
describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do
context 'when a value has not been stored' do
- it 'does not call #any_hook_failed?' do
+ it 'calls #any_hook_failed?' do
expect(object.get_web_hook_failure).to be_nil
expect(object).to receive(:any_hook_failed?).and_return(true)
diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
index 5eeefacdeb9..3f532629961 100644
--- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
+++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
@@ -290,6 +290,7 @@ RSpec.shared_examples 'value stream analytics label based stage' do
context 'when `ProjectLabel is given' do
let_it_be(:label) { create(:label) }
+ let(:expected_error) { s_('CycleAnalyticsStage|is not available for the selected group') }
it 'raises error when `ProjectLabel` is given for `start_event_label`' do
params = {
@@ -300,7 +301,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do
end_event_identifier: :issue_closed
}
- expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ stage = described_class.new(params)
+ expect(stage).to be_invalid
+ expect(stage.errors.messages_for(:start_event_label_id)).to eq([expected_error])
end
it 'raises error when `ProjectLabel` is given for `end_event_label`' do
@@ -312,7 +315,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do
end_event_label: label
}
- expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ stage = described_class.new(params)
+ expect(stage).to be_invalid
+ expect(stage.errors.messages_for(:end_event_label_id)).to eq([expected_error])
end
end
end
diff --git a/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb b/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb
new file mode 100644
index 00000000000..3d98d9136e2
--- /dev/null
+++ b/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'database events tracking' do
+ describe 'events tracking' do
+ # required definitions:
+ # :record, :update_params
+ #
+ # other available attributes:
+ # :project, :namespace
+
+ let(:user) { nil }
+ let(:category) { described_class.to_s }
+ let(:label) { described_class.table_name }
+ let(:action) { "database_event_#{property}" }
+ let(:feature_flag_name) { :product_intelligence_database_event_tracking }
+ let(:record_tracked_attributes) { record.attributes.slice(*described_class::SNOWPLOW_ATTRIBUTES.map(&:to_s)) }
+ let(:base_extra) { record_tracked_attributes.merge(project: try(:project), namespace: try(:namespace)) }
+
+ before do
+ allow(Gitlab::Tracking).to receive(:database_event).and_call_original
+ end
+
+ describe '#create' do
+ it_behaves_like 'Snowplow event tracking', overrides: { tracking_method: :database_event } do
+ subject(:create_record) { record }
+
+ let(:extra) { base_extra }
+ let(:property) { 'create' }
+ end
+ end
+
+ describe '#update', :freeze_time do
+ it_behaves_like 'Snowplow event tracking', overrides: { tracking_method: :database_event } do
+ subject(:update_record) { record.update!(update_params) }
+
+ let(:extra) { base_extra.merge(update_params.stringify_keys) }
+ let(:property) { 'update' }
+ end
+ end
+
+ describe '#destroy' do
+ it_behaves_like 'Snowplow event tracking', overrides: { tracking_method: :database_event } do
+ subject(:delete_record) { record.destroy! }
+
+ let(:extra) { base_extra }
+ let(:property) { 'destroy' }
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'database events tracking batch 2' do
+ it_behaves_like 'database events tracking' do
+ let(:feature_flag_name) { :product_intelligence_database_event_tracking_batch2 }
+ end
+end
diff --git a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
index 64390ccdc25..f1f6d799cf3 100644
--- a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
@@ -10,10 +10,12 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'raises an error' do
allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(nil)
- expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError,
- "Failed to find diff line for: #{diff_file_from_repository.file_path}, "\
- "old_line: #{position.old_line}"\
- ", new_line: #{position.new_line}")
+ expect { subject.save! }.to raise_error(
+ ::DiffNote::NoteDiffFileCreationError,
+ "Failed to find diff line for: #{diff_file_from_repository.file_path}, "\
+ "old_line: #{position.old_line}"\
+ ", new_line: #{position.new_line}"
+ )
end
end
diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
index 7dfdd24177e..0cf109ce5c5 100644
--- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
@@ -3,7 +3,6 @@
RSpec.shared_examples Integrations::BaseSlashCommands do
describe "Associations" do
it { is_expected.to respond_to :token }
- it { is_expected.to have_many :chat_names }
end
describe 'default values' do
@@ -85,7 +84,7 @@ RSpec.shared_examples Integrations::BaseSlashCommands do
end
context 'when the user is authenticated' do
- let!(:chat_name) { create(:chat_name, integration: subject) }
+ let!(:chat_name) { create(:chat_name) }
let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
subject do
diff --git a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb
index 6d519e561ee..d438918eb60 100644
--- a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb
@@ -10,19 +10,19 @@ end
RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
it 'allows underscores in the project name' do
- expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end
it 'allows numbers in the project name' do
- expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
+ expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
end
it 'requires the project name to begin with A-Z' do
- expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil
- expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil
+ expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end
it 'does not allow issue number to finish with a letter' do
- expect(described_class.reference_pattern.match('EXT-123A')).to eq(nil)
+ expect(subject.reference_pattern.match('EXT-123A')).to eq(nil)
end
end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 7159c55e303..e9e25dee746 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -392,6 +392,30 @@ RSpec.shared_examples_for "bulk member creation" do
expect(members.first).to be_invite
end
+ context 'with different source types' do
+ shared_examples 'supports multiple sources' do
+ specify do
+ members = described_class.add_members(sources, [user1, user2], :maintainer)
+
+ expect(members.map(&:user)).to contain_exactly(user1, user2, user1, user2)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
+ end
+
+ context 'with an array of sources' do
+ let_it_be(:sources) { [source, source2] }
+
+ it_behaves_like 'supports multiple sources'
+ end
+
+ context 'with a query producing sources' do
+ let_it_be(:sources) { source_type.id_in([source, source2]) }
+
+ it_behaves_like 'supports multiple sources'
+ end
+ end
+
context 'with de-duplication' do
it 'has the same user by id and user' do
members = described_class.add_members(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer)
@@ -484,11 +508,13 @@ RSpec.shared_examples_for "bulk member creation" do
create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
expect do
- described_class.add_members(source,
- [user1.id],
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id)
+ described_class.add_members(
+ source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id
+ )
end.not_to change { MemberTask.count }
member.reset
@@ -498,11 +524,13 @@ RSpec.shared_examples_for "bulk member creation" do
it 'adds tasks to be done if they do not exist', :aggregate_failures do
expect do
- described_class.add_members(source,
- [user1.id],
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id)
+ described_class.add_members(
+ source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id
+ )
end.to change { MemberTask.count }.by(1)
member = source.members.find_by(user_id: user1.id)
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
index e28220334ac..329cb812a08 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -69,7 +69,7 @@ RSpec.shared_examples 'members notifications' do |entity_type|
let(:member) { create(:"#{entity_type}_member", :invited) }
it "calls NotificationService.decline_#{entity_type}_invite" do
- expect(notification_service).to receive(:"decline_#{entity_type}_invite").with(member)
+ expect(notification_service).to receive(:decline_invite).with(member)
member.decline_invite!
end
diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
index 5be0f6349ea..c2c123277ee 100644
--- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
+++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
@@ -20,7 +20,6 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
let_it_be(:component_file_other_architecture, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_2) }
let_it_be(:component_file_other_component, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_2, architecture: architecture1_1) }
let_it_be(:component_file_other_compression_type, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, compression_type: :xz) }
- let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') }
let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') }
let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) }
let_it_be_with_refind(:component_file_with_file_type_sources) { create("debian_#{container_type}_component_file", :sources, component: component1_1) }
@@ -100,10 +99,6 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
it { is_expected.to validate_presence_of(:file_store) }
end
- describe '#file_md5' do
- it { is_expected.to validate_presence_of(:file_md5) }
- end
-
describe '#file_sha256' do
it { is_expected.to validate_presence_of(:file_sha256) }
end
@@ -231,4 +226,20 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") }
end
end
+
+ describe '#empty?' do
+ subject { component_file_with_architecture.empty? }
+
+ context 'with a non-empty component' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with an empty component' do
+ before do
+ component_file_with_architecture.update! size: 0
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index ac4ad4525aa..3ea2ff4d8f0 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -1,32 +1,13 @@
# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
- let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, :with_suite) }
- let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: distribution_with_suite.container ) }
- let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) }
- let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) }
- let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) }
-
- let_it_be_with_refind(:distribution) { create(factory, container: distribution_with_suite.container ) }
-
+RSpec.shared_examples 'Debian Distribution for common behavior' do
subject { distribution }
describe 'relationships' do
- it { is_expected.to belong_to(container) }
it { is_expected.to belong_to(:creator).class_name('User') }
-
- it { is_expected.to have_one(:key).class_name("Packages::Debian::#{container.capitalize}DistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) }
- it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) }
- it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) }
end
describe 'validations' do
- describe "##{container}" do
- it { is_expected.to validate_presence_of(container) }
- end
-
describe "#creator" do
it { is_expected.not_to validate_presence_of(:creator) }
end
@@ -47,57 +28,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it { is_expected.not_to allow_value('hé').for(:suite) }
end
- describe '#unique_debian_suite_and_codename' do
- using RSpec::Parameterized::TableSyntax
-
- where(:with_existing_suite, :suite, :codename, :errors) do
- false | nil | :keep | nil
- false | 'testing' | :keep | nil
- false | nil | :codename | ["Codename has already been taken"]
- false | :codename | :keep | ["Suite has already been taken as Codename"]
- false | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"]
- true | nil | :keep | nil
- true | 'testing' | :keep | nil
- true | nil | :codename | ["Codename has already been taken"]
- true | :codename | :keep | ["Suite has already been taken as Codename"]
- true | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"]
- true | nil | :suite | ["Codename has already been taken as Suite"]
- true | :suite | :keep | ["Suite has already been taken"]
- true | :suite | :suite | ["Suite has already been taken", "Codename has already been taken as Suite"]
- end
-
- with_them do
- context factory do
- let(:new_distribution) { build(factory, container: distribution.container) }
-
- before do
- distribution.update_column(:suite, 'suite-' + distribution.codename) if with_existing_suite
-
- if suite.is_a?(Symbol)
- new_distribution.suite = distribution.send suite unless suite == :keep
- else
- new_distribution.suite = suite
- end
-
- if codename.is_a?(Symbol)
- new_distribution.codename = distribution.send codename unless codename == :keep
- else
- new_distribution.codename = codename
- end
- end
-
- it do
- if errors
- expect(new_distribution).not_to be_valid
- expect(new_distribution.errors.to_a).to eq(errors)
- else
- expect(new_distribution).to be_valid
- end
- end
- end
- end
- end
-
describe '#origin' do
it { is_expected.to allow_value(nil).for(:origin) }
it { is_expected.to allow_value('Debian').for(:origin) }
@@ -179,7 +109,11 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
subject { described_class.with_codename_or_suite(distribution_with_suite.codename) }
it 'does not return other distributions' do
- expect(subject.to_a).to contain_exactly(distribution_with_suite, distribution_with_same_codename, distribution_with_codename_and_suite_flipped)
+ expect(subject.to_a)
+ .to contain_exactly(
+ distribution_with_suite,
+ distribution_with_same_codename,
+ distribution_with_codename_and_suite_flipped)
end
end
@@ -187,54 +121,169 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
subject { described_class.with_codename_or_suite(distribution_with_suite.suite) }
it 'does not return other distributions' do
- expect(subject.to_a).to contain_exactly(distribution_with_suite, distribution_with_same_suite, distribution_with_codename_and_suite_flipped)
+ expect(subject.to_a)
+ .to contain_exactly(
+ distribution_with_suite,
+ distribution_with_same_suite,
+ distribution_with_codename_and_suite_flipped)
end
end
end
end
+end
- if container == :project
- describe 'project distribution specifics' do
- describe 'relationships' do
- it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) }
- it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) }
- end
+RSpec.shared_examples 'Debian Distribution for specific behavior' do |factory|
+ describe '#unique_debian_suite_and_codename' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:with_existing_suite, :suite, :codename, :errors) do
+ false | nil | :keep | nil
+ false | 'testing' | :keep | nil
+ false | nil | :codename | ["Codename has already been taken"]
+ false | :codename | :keep | ["Suite has already been taken as Codename"]
+ false | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"]
+ true | nil | :keep | nil
+ true | 'testing' | :keep | nil
+ true | nil | :codename | ["Codename has already been taken"]
+ true | :codename | :keep | ["Suite has already been taken as Codename"]
+ true | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"]
+ true | nil | :suite | ["Codename has already been taken as Suite"]
+ true | :suite | :keep | ["Suite has already been taken"]
+ true | :suite | :suite | ["Suite has already been taken", "Codename has already been taken as Suite"]
end
- else
- describe 'group distribution specifics' do
- let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container) }
- let_it_be(:public_distribution_with_same_codename) { create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename) }
- let_it_be(:public_package_with_same_codename) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename) }
- let_it_be(:public_distribution_with_same_suite) { create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite) }
- let_it_be(:public_package_with_same_suite) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite) }
-
- let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container) }
- let_it_be(:private_distribution_with_same_codename) { create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename) }
- let_it_be(:private_package_with_same_codename) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) }
- let_it_be(:private_distribution_with_same_suite) { create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite) }
- let_it_be(:private_package_with_same_suite) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) }
-
- describe '#packages' do
- subject { distribution_with_suite.packages }
-
- it 'returns only public packages with same codename' do
- expect(subject.to_a).to contain_exactly(public_package_with_same_codename)
+
+ with_them do
+ context factory do
+ let(:new_distribution) { build(factory, container: distribution.container) }
+
+ before do
+ distribution.update_column(:suite, "suite-#{distribution.codename}") if with_existing_suite
+
+ if suite.is_a?(Symbol)
+ new_distribution.suite = distribution.send suite unless suite == :keep
+ else
+ new_distribution.suite = suite
+ end
+
+ if codename.is_a?(Symbol)
+ new_distribution.codename = distribution.send codename unless codename == :keep
+ else
+ new_distribution.codename = codename
+ end
+ end
+
+ it do
+ if errors
+ expect(new_distribution).not_to be_valid
+ expect(new_distribution.errors.to_a).to eq(errors)
+ else
+ expect(new_distribution).to be_valid
+ end
end
end
+ end
+ end
+end
- describe '#package_files' do
- subject { distribution_with_suite.package_files }
+RSpec.shared_examples 'Debian Distribution with project container' do
+ it_behaves_like 'Debian Distribution for specific behavior', :debian_project_distribution
- it 'returns only files from public packages with same codename' do
- expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files)
- end
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
- context 'with pending destruction package files' do
- let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: public_package_with_same_codename) }
+ it { is_expected.to have_one(:key).class_name("Packages::Debian::ProjectDistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) }
+ it { is_expected.to have_many(:components).class_name("Packages::Debian::ProjectComponent").inverse_of(:distribution) }
+ it { is_expected.to have_many(:architectures).class_name("Packages::Debian::ProjectArchitecture").inverse_of(:distribution) }
+ end
- it 'does not return them' do
- expect(subject.to_a).not_to include(package_file_pending_destruction)
- end
+ describe "#project" do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+
+ describe 'project distribution specifics' do
+ describe 'relationships' do
+ it do
+ is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution)
+ .with_foreign_key(:distribution_id)
+ end
+
+ it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) }
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian Distribution with group container' do
+ it_behaves_like 'Debian Distribution for specific behavior', :debian_group_distribution
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:group) }
+
+ it { is_expected.to have_one(:key).class_name("Packages::Debian::GroupDistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) }
+ it { is_expected.to have_many(:components).class_name("Packages::Debian::GroupComponent").inverse_of(:distribution) }
+ it { is_expected.to have_many(:architectures).class_name("Packages::Debian::GroupArchitecture").inverse_of(:distribution) }
+ end
+
+ describe "#group" do
+ it { is_expected.to validate_presence_of(:group) }
+ end
+
+ describe 'group distribution specifics' do
+ let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container) }
+ let_it_be(:public_distribution_with_same_codename) do
+ create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename)
+ end
+
+ let_it_be(:public_package_with_same_codename) do
+ create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename)
+ end
+
+ let_it_be(:public_distribution_with_same_suite) do
+ create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite)
+ end
+
+ let_it_be(:public_package_with_same_suite) do
+ create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite)
+ end
+
+ let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container) }
+ let_it_be(:private_distribution_with_same_codename) do
+ create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename)
+ end
+
+ let_it_be(:private_package_with_same_codename) do
+ create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)
+ end
+
+ let_it_be(:private_distribution_with_same_suite) do
+ create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite)
+ end
+
+ let_it_be(:private_package_with_same_suite) do
+ create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)
+ end
+
+ describe '#packages' do
+ subject { distribution_with_suite.packages }
+
+ it 'returns only public packages with same codename' do
+ expect(subject.to_a).to contain_exactly(public_package_with_same_codename)
+ end
+ end
+
+ describe '#package_files' do
+ subject { distribution_with_suite.package_files }
+
+ it 'returns only files from public packages with same codename' do
+ expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files)
+ end
+
+ context 'with pending destruction package files' do
+ let_it_be(:package_file_pending_destruction) do
+ create(:package_file, :pending_destruction, package: public_package_with_same_codename)
+ end
+
+ it 'does not return them' do
+ expect(subject.to_a).not_to include(package_file_pending_destruction)
end
end
end
diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
index 3caf58da4d2..f1af1760e8d 100644
--- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
@@ -19,7 +19,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do
end
end
-RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''|
+RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: '', default: false|
using RSpec::Parameterized::TableSyntax
context 'when ci_cd_settings is nil' do
@@ -28,7 +28,7 @@ RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''|
end
it 'returns false' do
- expect(project.send("#{prefix}#{delegated_method}")).to be(false)
+ expect(project.send("#{prefix}#{delegated_method}")).to be(default)
end
end
diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb
index 038ff33c68a..1409f7caea8 100644
--- a/spec/support/shared_examples/models/resource_event_shared_examples.rb
+++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb
@@ -10,6 +10,8 @@ RSpec.shared_examples 'a resource event' do
let_it_be(:issue2) { create(:issue, author: user1) }
let_it_be(:issue3) { create(:issue, author: user2) }
+ let(:resource_event) { described_class.name.demodulize.underscore.to_sym }
+
describe 'importable' do
it { is_expected.to respond_to(:importing?) }
it { is_expected.to respond_to(:imported?) }
@@ -36,9 +38,9 @@ RSpec.shared_examples 'a resource event' do
let!(:created_at2) { 2.days.ago }
let!(:created_at3) { 3.days.ago }
- let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) }
- let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) }
- let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) }
+ let!(:event1) { create(resource_event, issue: issue1, created_at: created_at1) }
+ let!(:event2) { create(resource_event, issue: issue2, created_at: created_at2) }
+ let!(:event3) { create(resource_event, issue: issue2, created_at: created_at3) }
it 'returns the expected events' do
events = described_class.created_after(created_at3)
@@ -62,9 +64,10 @@ RSpec.shared_examples 'a resource event for issues' do
let_it_be(:issue2) { create(:issue, author: user1) }
let_it_be(:issue3) { create(:issue, author: user2) }
- let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) }
- let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) }
- let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) }
+ let_it_be(:resource_event) { described_class.name.demodulize.underscore.to_sym }
+ let_it_be(:event1) { create(resource_event, issue: issue1) }
+ let_it_be(:event2) { create(resource_event, issue: issue2) }
+ let_it_be(:event3) { create(resource_event, issue: issue1) }
describe 'associations' do
it { is_expected.to belong_to(:issue) }
@@ -93,9 +96,9 @@ RSpec.shared_examples 'a resource event for issues' do
end
describe '.by_created_at_earlier_or_equal_to' do
- let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') }
- let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') }
- let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') }
+ let_it_be(:event1) { create(resource_event, issue: issue1, created_at: '2020-03-10') }
+ let_it_be(:event2) { create(resource_event, issue: issue2, created_at: '2020-03-10') }
+ let_it_be(:event3) { create(resource_event, issue: issue1, created_at: '2020-03-12') }
it 'returns the expected events' do
events = described_class.by_created_at_earlier_or_equal_to('2020-03-11 23:59:59')
@@ -112,7 +115,7 @@ RSpec.shared_examples 'a resource event for issues' do
if described_class.method_defined?(:issuable)
describe '#issuable' do
- let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) }
+ let_it_be(:event1) { create(resource_event, issue: issue2) }
it 'returns the expected issuable' do
expect(event1.issuable).to eq(issue2)
@@ -125,6 +128,7 @@ RSpec.shared_examples 'a resource event for merge requests' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
+ let_it_be(:resource_event) { described_class.name.demodulize.underscore.to_sym }
let_it_be(:merge_request1) { create(:merge_request, author: user1) }
let_it_be(:merge_request2) { create(:merge_request, author: user1) }
let_it_be(:merge_request3) { create(:merge_request, author: user2) }
@@ -134,9 +138,9 @@ RSpec.shared_examples 'a resource event for merge requests' do
end
describe '.by_merge_request' do
- let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) }
- let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) }
- let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) }
+ let_it_be(:event1) { create(resource_event, merge_request: merge_request1) }
+ let_it_be(:event2) { create(resource_event, merge_request: merge_request2) }
+ let_it_be(:event3) { create(resource_event, merge_request: merge_request1) }
it 'returns the expected records for an issue with events' do
events = described_class.by_merge_request(merge_request1)
@@ -153,7 +157,7 @@ RSpec.shared_examples 'a resource event for merge requests' do
if described_class.method_defined?(:issuable)
describe '#issuable' do
- let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) }
+ let_it_be(:event1) { create(resource_event, merge_request: merge_request2) }
it 'returns the expected issuable' do
expect(event1.issuable).to eq(merge_request2)
@@ -163,7 +167,7 @@ RSpec.shared_examples 'a resource event for merge requests' do
context 'on callbacks' do
it 'does not trigger note created subscription' do
- event = build(described_class.name.underscore.to_sym, merge_request: merge_request1)
+ event = build(resource_event, merge_request: merge_request1)
expect(GraphqlTriggers).not_to receive(:work_item_note_created)
expect(event).not_to receive(:trigger_note_subscription_create)
@@ -177,15 +181,17 @@ RSpec.shared_examples 'a note for work item resource event' do
let_it_be(:project) { create(:project) }
let_it_be(:work_item) { create(:work_item, :task, project: project, author: user) }
+ let(:resource_event) { described_class.name.demodulize.underscore.to_sym }
+
it 'builds synthetic note with correct synthetic_note_class' do
- event = build(described_class.name.underscore.to_sym, issue: work_item)
+ event = build(resource_event, issue: work_item)
expect(event.work_item_synthetic_system_note.class.name).to eq(event.synthetic_note_class.name)
end
context 'on callbacks' do
it 'triggers note created subscription' do
- event = build(described_class.name.underscore.to_sym, issue: work_item)
+ event = build(resource_event, issue: work_item)
expect(GraphqlTriggers).to receive(:work_item_note_created)
expect(event).to receive(:trigger_note_subscription_create).and_call_original
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 7e69a6663d5..017e51ecd24 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -94,6 +94,40 @@ RSpec.shared_examples 'wiki model' do
end
end
+ describe '#has_home_page?' do
+ context 'when home page exists' do
+ before do
+ wiki.repository.create_file(
+ user,
+ 'home.md',
+ 'home file',
+ branch_name: wiki.default_branch,
+ message: "created home page",
+ author_email: user.email,
+ author_name: user.name
+ )
+ end
+
+ it 'returns true' do
+ expect(wiki.has_home_page?).to eq(true)
+ end
+
+ it 'returns false when #find_page raise an error' do
+ allow(wiki)
+ .to receive(:find_page)
+ .and_raise(StandardError)
+
+ expect(wiki.has_home_page?).to eq(false)
+ end
+ end
+
+ context 'when home page does not exist' do
+ it 'returns false' do
+ expect(wiki.has_home_page?).to eq(false)
+ end
+ end
+ end
+
describe '#to_global_id' do
it 'returns a global ID' do
expect(wiki.to_global_id.to_s).to eq("gid://gitlab/#{wiki.class.name}/#{wiki.id}")
@@ -791,6 +825,21 @@ RSpec.shared_examples 'wiki model' do
end
end
+ context 'when the repository fails to update' do
+ let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') }
+
+ it 'returns false and sets error message', :aggregate_failures do
+ expect(subject.repository)
+ .to receive(:update_file)
+ .and_raise(Gitlab::Git::Index::IndexError.new)
+
+ expect(subject.update_page(page.page, content: 'new content', format: :markdown))
+ .to eq(false)
+ expect(subject.error_message)
+ .to match("Duplicate page: A page with that title already exists")
+ end
+ end
+
context 'when page path does not have a default extension' do
let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') }
diff --git a/spec/support/shared_examples/observability/csp_shared_examples.rb b/spec/support/shared_examples/observability/csp_shared_examples.rb
index 0cd211f69eb..9d6e7e75f4d 100644
--- a/spec/support/shared_examples/observability/csp_shared_examples.rb
+++ b/spec/support/shared_examples/observability/csp_shared_examples.rb
@@ -31,19 +31,19 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
let(:observability_url) { Gitlab::Observability.observability_url }
let(:signin_url) do
Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
- '/users/sign_in')
+ '/users/sign_in')
end
let(:oauth_url) do
Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
- '/oauth/authorize')
+ '/oauth/authorize')
end
before do
setup_csp_for_controller(controller_class, csp, any_time: true)
group.add_developer(user)
login_as(user)
- allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(true)
+ stub_feature_flags(observability_group_tab: true)
end
subject do
@@ -67,7 +67,7 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
end
before do
- allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(false)
+ stub_feature_flags(observability_group_tab: false)
end
it 'does not add observability urls to the csp header' do
@@ -76,23 +76,6 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
end
end
- context 'when checking if observability is enabled' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src 'https://something.test'
- end
- end
-
- it 'check access for a given user and group' do
- allow(Gitlab::Observability).to receive(:observability_enabled?)
-
- get tested_path
-
- expect(Gitlab::Observability).to have_received(:observability_enabled?)
- .with(user, group).at_least(:once)
- end
- end
-
context 'when frame-src exists in the CSP config' do
let(:csp) do
ActionDispatch::ContentSecurityPolicy.new do |p|
diff --git a/spec/support/shared_examples/observability/embed_observabilities_examples.rb b/spec/support/shared_examples/observability/embed_observabilities_examples.rb
new file mode 100644
index 00000000000..c8d4e9e0d7e
--- /dev/null
+++ b/spec/support/shared_examples/observability/embed_observabilities_examples.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'embeds observability' do
+ it 'renders iframe in description' do
+ page.within('.description') do
+ expect_observability_iframe(page.html)
+ end
+ end
+
+ it 'renders iframe in comment' do
+ expect(page).not_to have_css('.note-text')
+
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: observable_url)
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note-text') do
+ expect_observability_iframe(page.html)
+ end
+ end
+end
+
+RSpec.shared_examples 'does not embed observability' do
+ it 'does not render iframe in description' do
+ page.within('.description') do
+ expect_observability_iframe(page.html, to_be_nil: true)
+ end
+ end
+
+ it 'does not render iframe in comment' do
+ expect(page).not_to have_css('.note-text')
+
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: observable_url)
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note-text') do
+ expect_observability_iframe(page.html, to_be_nil: true)
+ end
+ end
+end
+
+def expect_observability_iframe(html, to_be_nil: false)
+ iframe = Nokogiri::HTML.parse(html).at_css('#observability-ui-iframe')
+
+ expect(html).to include(observable_url)
+
+ if to_be_nil
+ expect(iframe).to be_nil
+ else
+ expect(iframe).not_to be_nil
+ iframe_src = "#{expected_observable_url}&theme=light&username=#{user.username}&kiosk=inline-embed"
+ expect(iframe.attributes['src'].value).to eq(iframe_src)
+ end
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index 9ec1b8b3f5a..d1f5a01b10c 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -401,3 +401,24 @@ RSpec.shared_examples 'package access with repository disabled' do
it { is_expected.to be_allowed(:read_package) }
end
+
+RSpec.shared_examples 'equivalent project policy abilities' do
+ where(:project_visibility, :user_role_on_project) do
+ project_visibilities = [:public, :internal, :private]
+ user_role_on_project = [:anonymous, :non_member, :guest, :reporter, :developer, :maintainer, :owner, :admin]
+ project_visibilities.product(user_role_on_project)
+ end
+
+ with_them do
+ it 'evaluates the same' do
+ project = public_send("#{project_visibility}_project")
+ current_user = public_send(user_role_on_project)
+ enable_admin_mode!(current_user) if user_role_on_project == :admin
+ policy = ProjectPolicy.new(current_user, project)
+ old_permissions = policy.allowed?(old_policy)
+ new_permissions = policy.allowed?(new_policy)
+
+ expect(old_permissions).to eq new_permissions
+ end
+ end
+end
diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
index f70621673d5..f9f8435c211 100644
--- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
+++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
@@ -7,9 +7,9 @@ RSpec.shared_examples 'when regex matching everything is specified' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
context 'with deprecated name_regex param' do
let(:params) do
@@ -17,9 +17,9 @@ RSpec.shared_examples 'when regex matching everything is specified' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
end
end
@@ -31,9 +31,9 @@ RSpec.shared_examples 'when regex matching everything is specified and latest is
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
end
RSpec.shared_examples 'when delete regex matching specific tags is used' do
@@ -43,9 +43,9 @@ RSpec.shared_examples 'when delete regex matching specific tags is used' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: [%w[C D]]
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[C D]]
end
RSpec.shared_examples 'when delete regex matching specific tags is used with overriding allow regex' do
@@ -58,9 +58,9 @@ RSpec.shared_examples 'when delete regex matching specific tags is used with ove
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: [%w[D]]
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[D]]
context 'with name_regex_delete overriding deprecated name_regex' do
let(:params) do
@@ -71,9 +71,9 @@ RSpec.shared_examples 'when delete regex matching specific tags is used with ove
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: [%w[D]]
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[D]]
end
end
@@ -87,9 +87,9 @@ RSpec.shared_examples 'with allow regex value' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
end
RSpec.shared_examples 'when keeping only N tags' do
@@ -135,9 +135,9 @@ RSpec.shared_examples 'when removing keeping only 3' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
end
RSpec.shared_examples 'when removing older than 1 day' do
@@ -150,9 +150,9 @@ RSpec.shared_examples 'when removing older than 1 day' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
end
RSpec.shared_examples 'when combining all parameters' do
@@ -166,9 +166,9 @@ RSpec.shared_examples 'when combining all parameters' do
end
it_behaves_like 'removing the expected tags',
- service_response_extra: service_response_extra,
- supports_caching: supports_caching,
- delete_expectations: delete_expectations
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
end
RSpec.shared_examples 'when running a container_expiration_policy' do
diff --git a/spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb b/spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb
new file mode 100644
index 00000000000..d196114b227
--- /dev/null
+++ b/spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'additional metrics query' do
+ include Prometheus::MetricBuilders
+
+ let(:metric_group_class) { Gitlab::Prometheus::MetricGroup }
+ let(:metric_class) { Gitlab::Prometheus::Metric }
+
+ let(:metric_names) { %w[metric_a metric_b] }
+
+ let(:query_range_result) do
+ [{ metric: {}, values: [[1488758662.506, '0.00002996364761904785'], [1488758722.506, '0.00003090239047619091']] }]
+ end
+
+ let(:client) { instance_double('Gitlab::PrometheusClient') }
+ let(:query_result) { described_class.new(client).query(*query_params) }
+ let(:project) { create(:project, :repository) }
+ let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
+
+ before do
+ allow(client).to receive(:label_values).and_return(metric_names)
+ allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group(metrics: [simple_metric])])
+ end
+
+ describe 'metrics query context' do
+ subject! { described_class.new(client) }
+
+ shared_examples 'query context containing environment slug and filter' do
+ it 'contains ci_environment_slug' do
+ expect(subject)
+ .to receive(:query_metrics).with(project, environment, hash_including(ci_environment_slug: environment.slug))
+
+ subject.query(*query_params)
+ end
+
+ it 'contains environment filter' do
+ expect(subject).to receive(:query_metrics).with(
+ project,
+ environment,
+ hash_including(
+ environment_filter: "container_name!=\"POD\",environment=\"#{environment.slug}\""
+ )
+ )
+
+ subject.query(*query_params)
+ end
+ end
+
+ describe 'project has Kubernetes service' do
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
+ let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
+ let(:kube_namespace) { environment.deployment_namespace }
+
+ it_behaves_like 'query context containing environment slug and filter'
+
+ it 'query context contains kube_namespace' do
+ expect(subject)
+ .to receive(:query_metrics).with(project, environment, hash_including(kube_namespace: kube_namespace))
+
+ subject.query(*query_params)
+ end
+ end
+ end
+
+ describe 'project without Kubernetes service' do
+ it_behaves_like 'query context containing environment slug and filter'
+
+ it 'query context contains empty kube_namespace' do
+ expect(subject).to receive(:query_metrics).with(project, environment, hash_including(kube_namespace: ''))
+
+ subject.query(*query_params)
+ end
+ end
+ end
+
+ context 'with one group where two metrics is found' do
+ before do
+ allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group])
+ end
+
+ context 'when some queries return results' do
+ before do
+ allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result)
+ allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result)
+ allow(client).to receive(:query_range).with('query_range_empty', any_args).and_return([])
+ end
+
+ it 'return group data only for queries with results' do
+ expected = [
+ {
+ group: 'name',
+ priority: 1,
+ metrics: [
+ {
+ title: 'title', weight: 1, y_label: 'Values', queries: [
+ { query_range: 'query_range_a', result: query_range_result },
+ { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result }
+ ]
+ }
+ ]
+ }
+ ]
+
+ expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result')
+ expect(query_result).to eq(expected)
+ end
+ end
+ end
+
+ context 'with two groups with one metric each' do
+ let(:metrics) { [simple_metric(queries: [simple_query])] }
+
+ before do
+ allow(metric_group_class).to receive(:common_metrics).and_return(
+ [
+ simple_metric_group(name: 'group_a', metrics: [simple_metric(queries: [simple_query])]),
+ simple_metric_group(name: 'group_b', metrics: [simple_metric(title: 'title_b', queries: [simple_query('b')])])
+ ])
+ allow(client).to receive(:label_values).and_return(metric_names)
+ end
+
+ context 'when both queries return results' do
+ before do
+ allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result)
+ allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result)
+ end
+
+ it 'return group data both queries' do
+ queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] }
+ queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] }
+
+ expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result')
+
+ expect(query_result.count).to eq(2)
+ expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 })
+
+ expect(query_result[0][:metrics].first).to include(queries_with_result_a)
+ expect(query_result[1][:metrics].first).to include(queries_with_result_b)
+ end
+ end
+
+ context 'when one query returns result' do
+ before do
+ allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result)
+ allow(client).to receive(:query_range).with('query_range_b', any_args).and_return([])
+ end
+
+ it 'return group data only for query with results' do
+ queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] }
+
+ expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result')
+
+ expect(query_result.count).to eq(1)
+ expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 })
+
+ expect(query_result.first[:metrics].first).to include(queries_with_result)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb
new file mode 100644
index 00000000000..f308b4ad372
--- /dev/null
+++ b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "protected tags > access control > CE" do
+ ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected tags that #{access_type_name} can create" do
+ visit project_protected_tags_path(project)
+
+ set_protected_tag_name('master')
+ set_allowed_to('create', access_type_name)
+ click_on_protect
+
+ expect(ProtectedTag.count).to eq(1)
+ expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected tags so that #{access_type_name} can create them" do
+ visit project_protected_tags_path(project)
+
+ set_protected_tag_name('master')
+ set_allowed_to('create', 'No one')
+ click_on_protect
+
+ expect(ProtectedTag.count).to eq(1)
+
+ set_allowed_to('create', access_type_name, form: '.protected-tags-list')
+
+ wait_for_requests
+
+ expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index d8690356f81..7cbaf40721a 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'close quick action' do |issuable_type|
- include Spec::Support::Helpers::Features::NotesHelpers
+ include Features::NotesHelpers
before do
project.add_maintainer(maintainer)
diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
index b5704ad8f17..9b03cdbb3bf 100644
--- a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
@@ -23,11 +23,11 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do
end
note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: noteable_type,
- noteable_id: issuable.id,
- confidential: false
- )).execute
+ note: note_text,
+ noteable_type: noteable_type,
+ noteable_id: issuable.id,
+ confidential: false
+ )).execute
expect(note.errors[:validation]).to match_array([validation_message])
end
@@ -44,11 +44,11 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do
end
note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: noteable_type,
- noteable_id: issuable.id,
- confidential: false
- )).execute
+ note: note_text,
+ noteable_type: noteable_type,
+ noteable_id: issuable.id,
+ confidential: false
+ )).execute
expect(note.errors[:validation]).to be_empty
end
diff --git a/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb
new file mode 100644
index 00000000000..811b5ee4de2
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issues link quick action' do |command|
+ let_it_be_with_refind(:group) { create(:group) }
+ let_it_be_with_reload(:other_issue) { create(:issue, project: project) }
+ let_it_be_with_reload(:second_issue) { create(:issue, project: project) }
+ let_it_be_with_reload(:third_issue) { create(:issue, project: project) }
+
+ let(:link_type) { command == :relate ? 'relates_to' : 'blocks' }
+ let(:links_query) do
+ if command == :blocked_by
+ IssueLink.where(target: issue, link_type: link_type).map(&:source)
+ else
+ IssueLink.where(source: issue, link_type: link_type).map(&:target)
+ end
+ end
+
+ shared_examples 'link command' do
+ it 'links issues' do
+ service.execute(content, issue)
+
+ expect(links_query).to match_array(issues_linked)
+ end
+ end
+
+ context 'when user is member of group' do
+ before do
+ group.add_developer(user)
+ end
+
+ context 'when linking a single issue' do
+ let(:issues_linked) { [other_issue] }
+ let(:content) { "/#{command} #{other_issue.to_reference}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking multiple issues at once' do
+ let(:issues_linked) { [second_issue, third_issue] }
+ let(:content) { "/#{command} #{second_issue.to_reference} #{third_issue.to_reference}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when quick action target is unpersisted' do
+ let(:issue) { build(:issue, project: project) }
+ let(:issues_linked) { [other_issue] }
+ let(:content) { "/#{command} #{other_issue.to_reference}" }
+
+ it 'links the issues after the issue is persisted' do
+ service.execute(content, issue)
+
+ issue.save!
+
+ expect(links_query).to match_array(issues_linked)
+ end
+ end
+
+ context 'with empty link command' do
+ let(:issues_linked) { [] }
+ let(:content) { "/#{command}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'with already having linked issues' do
+ let(:issues_linked) { [second_issue, third_issue] }
+ let(:content) { "/#{command} #{third_issue.to_reference(project)}" }
+
+ before do
+ create_existing_link(command)
+ end
+
+ it_behaves_like 'link command'
+ end
+
+ context 'with cross project' do
+ let_it_be_with_reload(:another_group) { create(:group, :public) }
+ let_it_be_with_reload(:other_project) { create(:project, group: another_group) }
+
+ before do
+ another_group.add_developer(user)
+ [other_issue, second_issue, third_issue].map { |i| i.update!(project: other_project) }
+ end
+
+ context 'when linking a cross project issue' do
+ let(:issues_linked) { [other_issue] }
+ let(:content) { "/#{command} #{other_issue.to_reference(project)}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking multiple cross projects issues at once' do
+ let(:issues_linked) { [second_issue, third_issue] }
+ let(:content) { "/#{command} #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking a non-existing issue' do
+ let(:issues_linked) { [] }
+ let(:content) { "/#{command} imaginary##{non_existing_record_iid}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking a private issue' do
+ let_it_be(:private_issue) { create(:issue, project: create(:project, :private)) }
+ let(:issues_linked) { [] }
+ let(:content) { "/#{command} #{private_issue.to_reference(project)}" }
+
+ it_behaves_like 'link command'
+ end
+ end
+ end
+
+ def create_existing_link(command)
+ issues = [issue, second_issue]
+ source, target = command == :blocked_by ? issues.reverse : issues
+
+ create(:issue_link, source: source, target: target, link_type: link_type)
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb
index 3f1a98ca08e..7bd7500d546 100644
--- a/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'promote_to_incident quick action' do
+ include ListboxHelpers
+
describe '/promote_to_incident' do
context 'when issue can be promoted' do
it 'promotes issue to incident' do
@@ -52,9 +54,11 @@ RSpec.shared_examples 'promote_to_incident quick action' do
context 'when incident is selected for issue type' do
it 'promotes issue to incident' do
visit new_project_issue_path(project)
+ wait_for_requests
+
fill_in('Title', with: 'Title')
find('.js-issuable-type-filter-dropdown-wrap').click
- click_link('Incident')
+ select_listbox_item(_('Incident'))
fill_in('Description', with: '/promote_to_incident')
click_button('Create issue')
diff --git a/spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb b/spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb
new file mode 100644
index 00000000000..4a3732efe13
--- /dev/null
+++ b/spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_class|
+ include TmpdirHelper
+
+ let(:instance_specific_config_file) { "config/redis.#{name}.yml" }
+ let(:fallback_config_file) { nil }
+ let(:rails_root) { mktmpdir }
+
+ before do
+ allow(fallback_class).to receive(:config_file_name).and_return(fallback_config_file)
+ end
+
+ it_behaves_like "redis_shared_examples"
+
+ describe '#fetch_config' do
+ subject { described_class.new('test').send(:fetch_config) }
+
+ before do
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ end
+
+ context 'when redis.yml exists' do
+ before do
+ allow(described_class).to receive(:config_file_name).and_call_original
+ allow(described_class).to receive(:redis_yml_path).and_call_original
+ end
+
+ context 'when the fallback has a redis.yml entry' do
+ before do
+ File.write(File.join(rails_root, 'config/redis.yml'), {
+ 'test' => {
+ described_class.config_fallback.store_name.underscore => { 'fallback redis.yml' => 123 }
+ }
+ }.to_json)
+ end
+
+ it { expect(subject).to eq({ 'fallback redis.yml' => 123 }) }
+
+ context 'and an instance config file exists' do
+ before do
+ File.write(File.join(rails_root, instance_specific_config_file), {
+ 'test' => { 'instance specific file' => 456 }
+ }.to_json)
+ end
+
+ it { expect(subject).to eq({ 'instance specific file' => 456 }) }
+
+ context 'and the instance has a redis.yml entry' do
+ before do
+ File.write(File.join(rails_root, 'config/redis.yml'), {
+ 'test' => { name => { 'instance redis.yml' => 789 } }
+ }.to_json)
+ end
+
+ it { expect(subject).to eq({ 'instance redis.yml' => 789 }) }
+ end
+ end
+ end
+ end
+
+ context 'when no redis config file exsits' do
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'when resque.yml exists' do
+ before do
+ File.write(File.join(rails_root, 'config/resque.yml'), {
+ 'test' => { 'foobar' => 123 }
+ }.to_json)
+ end
+
+ it 'returns the config from resque.yml' do
+ expect(subject).to eq({ 'foobar' => 123 })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb
new file mode 100644
index 00000000000..9224e01b1fe
--- /dev/null
+++ b/spec/support/shared_examples/redis/redis_shared_examples.rb
@@ -0,0 +1,429 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "redis_shared_examples" do
+ include StubENV
+ include TmpdirHelper
+
+ let(:test_redis_url) { "redis://redishost:#{redis_port}" }
+ let(:test_cluster_config) { { cluster: [{ host: "redis://redishost", port: redis_port }] } }
+ let(:config_file_name) { instance_specific_config_file }
+ let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
+ let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
+ let(:old_socket_path) { "/path/to/old/redis.sock" }
+ let(:new_socket_path) { "/path/to/redis.sock" }
+ let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
+ let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
+ let(:config_cluster_format_host) { "spec/fixtures/config/redis_cluster_format_host.yml" }
+ let(:redis_port) { 6379 }
+ let(:redis_database) { 99 }
+ let(:sentinel_port) { 26379 }
+ let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml" }
+ let(:config_env_variable_url) { "TEST_GITLAB_REDIS_URL" }
+ let(:rails_root) { mktmpdir }
+
+ before do
+ allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s)
+ allow(described_class).to receive(:redis_yml_path).and_return('/dev/null')
+ end
+
+ describe '.config_file_name' do
+ subject { described_class.config_file_name }
+
+ before do
+ # Undo top-level stub of config_file_name because we are testing that method now.
+ allow(described_class).to receive(:config_file_name).and_call_original
+
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ end
+
+ context 'when there is no config file anywhere' do
+ it { expect(subject).to be_nil }
+ end
+ end
+
+ describe '.store' do
+ let(:rails_env) { 'development' }
+
+ subject { described_class.new(rails_env).store }
+
+ shared_examples 'redis store' do
+ let(:redis_store) { ::Redis::Store }
+ let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database}" }
+
+ it 'instantiates Redis::Store' do
+ is_expected.to be_a(redis_store)
+
+ expect(subject.to_s).to eq(redis_store_to_s)
+ end
+
+ context 'with the namespace' do
+ let(:namespace) { 'namespace_name' }
+ let(:redis_store_to_s) do
+ "Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}"
+ end
+
+ subject { described_class.new(rails_env).store(namespace: namespace) }
+
+ it "uses specified namespace" do
+ expect(subject.to_s).to eq(redis_store_to_s)
+ end
+ end
+ end
+
+ context 'with old format' do
+ it_behaves_like 'redis store' do
+ let(:config_file_name) { config_old_format_host }
+ let(:host) { "localhost:#{redis_port}" }
+ end
+ end
+
+ context 'with new format' do
+ it_behaves_like 'redis store' do
+ let(:config_file_name) { config_new_format_host }
+ let(:host) { "development-host:#{redis_port}" }
+ end
+ end
+ end
+
+ describe '.params' do
+ subject { described_class.new(rails_env).params }
+
+ let(:rails_env) { 'development' }
+ let(:config_file_name) { config_old_format_socket }
+
+ it 'withstands mutation' do
+ params1 = described_class.params
+ params2 = described_class.params
+ params1[:foo] = :bar
+
+ expect(params2).not_to have_key(:foo)
+ end
+
+ context 'when url contains unix socket reference' do
+ context 'with old format' do
+ let(:config_file_name) { config_old_format_socket }
+
+ it 'returns path key instead' do
+ is_expected.to include(path: old_socket_path)
+ is_expected.not_to have_key(:url)
+ end
+ end
+
+ context 'with new format' do
+ let(:config_file_name) { config_new_format_socket }
+
+ it 'returns path key instead' do
+ is_expected.to include(path: new_socket_path)
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+
+ context 'when url is host based' do
+ context 'with old format' do
+ let(:config_file_name) { config_old_format_host }
+
+ it 'returns hash with host, port, db, and password' do
+ is_expected.to include(host: 'localhost', password: 'mypassword', port: redis_port, db: redis_database)
+ is_expected.not_to have_key(:url)
+ end
+ end
+
+ context 'with new format' do
+ let(:config_file_name) { config_new_format_host }
+
+ where(:rails_env, :host) do
+ [
+ %w[development development-host],
+ %w[test test-host],
+ %w[production production-host]
+ ]
+ end
+
+ with_them do
+ it 'returns hash with host, port, db, and password' do
+ is_expected.to include(host: host, password: 'mynewpassword', port: redis_port, db: redis_database)
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+
+ context 'with redis cluster format' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ where(:rails_env, :host) do
+ [
+ %w[development development-master],
+ %w[test test-master],
+ %w[production production-master]
+ ]
+ end
+
+ with_them do
+ it 'returns hash with cluster and password' do
+ is_expected.to include(
+ password: 'myclusterpassword',
+ cluster: [
+ { host: "#{host}1", port: redis_port },
+ { host: "#{host}2", port: redis_port }
+ ]
+ )
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+ end
+ end
+
+ describe '.url' do
+ let(:config_file_name) { config_old_format_socket }
+
+ it 'withstands mutation' do
+ url1 = described_class.url
+ url2 = described_class.url
+ url1 << 'foobar' unless url1.frozen?
+
+ expect(url2).not_to end_with('foobar')
+ end
+
+ context 'when yml file with env variable' do
+ let(:config_file_name) { config_with_environment_variable_inside }
+
+ before do
+ stub_env(config_env_variable_url, test_redis_url)
+ end
+
+ it 'reads redis url from env variable' do
+ expect(described_class.url).to eq test_redis_url
+ end
+ end
+ end
+
+ describe '.version' do
+ it 'returns a version' do
+ expect(described_class.version).to be_present
+ end
+ end
+
+ describe '.with' do
+ let(:config_file_name) { config_old_format_socket }
+
+ before do
+ clear_pool
+ end
+
+ after do
+ clear_pool
+ end
+
+ context 'when running on single-threaded runtime' do
+ before do
+ allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(false)
+ end
+
+ it 'instantiates a connection pool with size 5' do
+ expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
+
+ described_class.with { |_redis_shared_example| true }
+ end
+ end
+
+ context 'when running on multi-threaded runtime' do
+ before do
+ allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(true)
+ allow(Gitlab::Runtime).to receive(:max_threads).and_return(18)
+ end
+
+ it 'instantiates a connection pool with a size based on the concurrency of the worker' do
+ expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
+
+ described_class.with { |_redis_shared_example| true }
+ end
+ end
+
+ context 'when there is no config at all' do
+ before do
+ # Undo top-level stub of config_file_name because we are testing that method now.
+ allow(described_class).to receive(:config_file_name).and_call_original
+
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ end
+
+ it 'can run an empty block' do
+ expect { described_class.with { nil } }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#db' do
+ let(:rails_env) { 'development' }
+
+ subject { described_class.new(rails_env).db }
+
+ context 'with old format' do
+ let(:config_file_name) { config_old_format_host }
+
+ it 'returns the correct db' do
+ expect(subject).to eq(redis_database)
+ end
+ end
+
+ context 'with new format' do
+ let(:config_file_name) { config_new_format_host }
+
+ it 'returns the correct db' do
+ expect(subject).to eq(redis_database)
+ end
+ end
+
+ context 'with cluster-mode' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ it 'returns the correct db' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ describe '#sentinels' do
+ subject { described_class.new(rails_env).sentinels }
+
+ let(:rails_env) { 'development' }
+
+ context 'when sentinels are defined' do
+ let(:config_file_name) { config_new_format_host }
+
+ where(:rails_env, :hosts) do
+ [
+ ['development', %w[development-replica1 development-replica2]],
+ ['test', %w[test-replica1 test-replica2]],
+ ['production', %w[production-replica1 production-replica2]]
+ ]
+ end
+
+ with_them do
+ it 'returns an array of hashes with host and port keys' do
+ is_expected.to include(host: hosts[0], port: sentinel_port)
+ is_expected.to include(host: hosts[1], port: sentinel_port)
+ end
+ end
+ end
+
+ context 'when sentinels are not defined' do
+ let(:config_file_name) { config_old_format_host }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when cluster is defined' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#sentinels?' do
+ subject { described_class.new(Rails.env).sentinels? }
+
+ context 'when sentinels are defined' do
+ let(:config_file_name) { config_new_format_host }
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when sentinels are not defined' do
+ let(:config_file_name) { config_old_format_host }
+
+ it { expect(subject).to eq(nil) }
+ end
+
+ context 'when cluster is defined' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+
+ describe '#raw_config_hash' do
+ it 'returns old-style single url config in a hash' do
+ expect(subject).to receive(:fetch_config) { test_redis_url }
+ expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url)
+ end
+
+ it 'returns cluster config without url key in a hash' do
+ expect(subject).to receive(:fetch_config) { test_cluster_config }
+ expect(subject.send(:raw_config_hash)).to eq(test_cluster_config)
+ end
+ end
+
+ describe '#fetch_config' do
+ before do
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ # Undo top-level stub of config_file_name because we are testing that method now.
+ allow(described_class).to receive(:config_file_name).and_call_original
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ end
+
+ it 'raises an exception when the config file contains invalid yaml' do
+ Tempfile.open('bad.yml') do |file|
+ file.write('{"not":"yaml"')
+ file.flush
+ allow(described_class).to receive(:config_file_name) { file.path }
+
+ expect { subject.send(:fetch_config) }.to raise_error(Psych::SyntaxError)
+ end
+ end
+
+ context 'when redis.yml exists' do
+ subject { described_class.new('test').send(:fetch_config) }
+
+ before do
+ allow(described_class).to receive(:redis_yml_path).and_call_original
+ end
+
+ it 'uses config/redis.yml' do
+ File.write(File.join(rails_root, 'config/redis.yml'), {
+ 'test' => { described_class.store_name.underscore => { 'foobar' => 123 } }
+ }.to_json)
+
+ expect(subject).to eq({ 'foobar' => 123 })
+ end
+ end
+
+ context 'when no config file exsits' do
+ subject { described_class.new('test').send(:fetch_config) }
+
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'when resque.yml exists' do
+ before do
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ File.write(File.join(rails_root, 'config/resque.yml'), {
+ 'test' => { 'foobar' => 123 }
+ }.to_json)
+ end
+
+ it 'returns the config from resque.yml' do
+ expect(subject).to eq({ 'foobar' => 123 })
+ end
+ end
+ end
+ end
+
+ def clear_pool
+ described_class.remove_instance_variable(:@pool)
+ rescue NameError
+ # raised if @pool was not set; ignore
+ end
+end
diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
index 2170025824f..74dbec063e0 100644
--- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'GET resource access tokens available' do
it 'lists all available scopes' do
get_access_tokens
- expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
+ expect(assigns(:scopes)).to eq(Gitlab::Auth.available_scopes_for(resource))
end
it 'returns for json response' do
diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
index 07fde7d3f35..4f198dfb740 100644
--- a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
+++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
@@ -1,98 +1,79 @@
# frozen_string_literal: true
-RSpec.shared_examples 'GET request permissions for admin mode' do
- it_behaves_like 'GET request permissions for admin mode when user'
- it_behaves_like 'GET request permissions for admin mode when admin'
-end
-
-RSpec.shared_examples 'PUT request permissions for admin mode' do |params|
- it_behaves_like 'PUT request permissions for admin mode when user', params
- it_behaves_like 'PUT request permissions for admin mode when admin', params
-end
-
-RSpec.shared_examples 'POST request permissions for admin mode' do |params|
- it_behaves_like 'POST request permissions for admin mode when user', params
- it_behaves_like 'POST request permissions for admin mode when admin', params
-end
RSpec.shared_examples 'DELETE request permissions for admin mode' do
- it_behaves_like 'DELETE request permissions for admin mode when user'
- it_behaves_like 'DELETE request permissions for admin mode when admin'
-end
-
-RSpec.shared_examples 'GET request permissions for admin mode when user' do
- subject { get api(path, current_user, admin_mode: admin_mode) }
+ subject { delete api(path, current_user, admin_mode: admin_mode) }
- let_it_be(:current_user) { create(:user) }
+ let_it_be(:success_status_code) { :no_content }
+ let_it_be(:failed_status_code) { :forbidden }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'when admin'
+ it_behaves_like 'when user'
end
-RSpec.shared_examples 'GET request permissions for admin mode when admin' do
+RSpec.shared_examples 'GET request permissions for admin mode' do
subject { get api(path, current_user, admin_mode: admin_mode) }
- let_it_be(:current_user) { create(:admin) }
-
- it_behaves_like 'admin mode on', true, :ok
- it_behaves_like 'admin mode on', false, :forbidden
-end
-
-RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params|
- subject { put api(path, current_user, admin_mode: admin_mode), params: params }
-
- let_it_be(:current_user) { create(:user) }
+ let_it_be(:success_status_code) { :ok }
+ let_it_be(:failed_status_code) { :forbidden }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'when admin'
+ it_behaves_like 'when user'
end
-RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params|
+RSpec.shared_examples 'PUT request permissions for admin mode' do
subject { put api(path, current_user, admin_mode: admin_mode), params: params }
- let_it_be(:current_user) { create(:admin) }
+ let_it_be(:success_status_code) { :ok }
+ let_it_be(:failed_status_code) { :forbidden }
- it_behaves_like 'admin mode on', true, :ok
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'when admin'
+ it_behaves_like 'when user'
end
-RSpec.shared_examples 'POST request permissions for admin mode when user' do |params|
+RSpec.shared_examples 'POST request permissions for admin mode' do
subject { post api(path, current_user, admin_mode: admin_mode), params: params }
- let_it_be(:current_user) { create(:user) }
+ let_it_be(:success_status_code) { :created }
+ let_it_be(:failed_status_code) { :forbidden }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'when admin'
+ it_behaves_like 'when user'
end
-RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params|
- subject { post api(path, current_user, admin_mode: admin_mode), params: params }
+RSpec.shared_examples 'when user' do
+ let_it_be(:current_user) { create(:user) }
- let_it_be(:current_user) { create(:admin) }
+ include_examples 'makes request' do
+ let(:status) { failed_status_code }
+ let(:admin_mode) { true }
+ end
- it_behaves_like 'admin mode on', true, :created
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'makes request' do
+ let(:status) { failed_status_code }
+ let(:admin_mode) { false }
+ end
end
-RSpec.shared_examples 'DELETE request permissions for admin mode when user' do
- subject { delete api(path, current_user, admin_mode: admin_mode) }
+RSpec.shared_examples 'when admin' do
+ let_it_be(:current_user) { create(:admin) }
- let_it_be(:current_user) { create(:user) }
+ it_behaves_like 'makes request' do
+ let(:status) { success_status_code }
+ let(:admin_mode) { true }
+ end
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'makes request' do
+ let(:status) { failed_status_code }
+ let(:admin_mode) { false }
+ end
end
-RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do
- subject { delete api(path, current_user, admin_mode: admin_mode) }
-
- let_it_be(:current_user) { create(:admin) }
-
- it_behaves_like 'admin mode on', true, :no_content
- it_behaves_like 'admin mode on', false, :forbidden
-end
+RSpec.shared_examples "makes request" do
+ let_it_be(:status) { nil }
-RSpec.shared_examples "admin mode on" do |admin_mode, status|
- let_it_be(:admin_mode) { admin_mode }
+ it "returns" do
+ subject
- it_behaves_like 'returning response status', status
+ expect(response).to have_gitlab_http_status(status)
+ end
end
diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
index f5c41416763..3ff52166990 100644
--- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc
subject
expect(json_response.length).to eq(2)
- expect(json_response.map { |repository| repository['id'] }).to contain_exactly(
+ expect(json_response.pluck('id')).to contain_exactly(
root_repository.id, test_repository.id)
expect(response.body).not_to include('tags')
expect(response.body).not_to include('tags_count')
@@ -47,7 +47,7 @@ RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope|
subject
expect(json_response.length).to eq(2)
- expect(json_response.map { |repository| repository['id'] }).to contain_exactly(
+ expect(json_response.pluck('id')).to contain_exactly(
root_repository.id, test_repository.id)
expect(response.body).to include('tags')
end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
index f31cbcfdec1..804221b7416 100644
--- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
let!(:custom_attribute1) { attributable.custom_attributes.create! key: 'foo', value: 'foo' }
let!(:custom_attribute2) { attributable.custom_attributes.create! key: 'bar', value: 'bar' }
- describe "GET /#{attributable_name} with custom attributes filter" do
+ describe "GET /#{attributable_name} with custom attributes filter", :aggregate_failures do
before do
other_attributable
end
@@ -14,13 +14,13 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { custom_attributes: { foo: 'foo', bar: 'bar' } }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.map { |r| r['id'] }).to include(attributable.id, other_attributable.id)
+ expect(json_response.pluck('id')).to include(attributable.id, other_attributable.id)
end
end
context 'with an authorized user' do
it 'filters by custom attributes' do
- get api("/#{attributable_name}", admin), params: { custom_attributes: { foo: 'foo', bar: 'bar' } }
+ get api("/#{attributable_name}", admin, admin_mode: true), params: { custom_attributes: { foo: 'foo', bar: 'bar' } }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to be 1
@@ -29,7 +29,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
- describe "GET /#{attributable_name} with custom attributes" do
+ describe "GET /#{attributable_name} with custom attributes", :aggregate_failures do
before do
other_attributable
end
@@ -46,7 +46,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
context 'with an authorized user' do
it 'does not include custom attributes by default' do
- get api("/#{attributable_name}", admin)
+ get api("/#{attributable_name}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to be_empty
@@ -54,7 +54,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
it 'includes custom attributes if requested' do
- get api("/#{attributable_name}", admin), params: { with_custom_attributes: true }
+ get api("/#{attributable_name}", admin, admin_mode: true), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to be_empty
@@ -72,7 +72,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
- describe "GET /#{attributable_name}/:id with custom attributes" do
+ describe "GET /#{attributable_name}/:id with custom attributes", :aggregate_failures do
context 'with an unauthorized user' do
it 'does not include custom attributes' do
get api("/#{attributable_name}/#{attributable.id}", user), params: { with_custom_attributes: true }
@@ -84,14 +84,14 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
context 'with an authorized user' do
it 'does not include custom attributes by default' do
- get api("/#{attributable_name}/#{attributable.id}", admin)
+ get api("/#{attributable_name}/#{attributable.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include 'custom_attributes'
end
it 'includes custom attributes if requested' do
- get api("/#{attributable_name}/#{attributable.id}", admin), params: { with_custom_attributes: true }
+ get api("/#{attributable_name}/#{attributable.id}", admin, admin_mode: true), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['custom_attributes']).to contain_exactly(
@@ -102,7 +102,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
- describe "GET /#{attributable_name}/:id/custom_attributes" do
+ describe "GET /#{attributable_name}/:id/custom_attributes", :aggregate_failures do
context 'with an unauthorized user' do
subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes", user) }
@@ -111,7 +111,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
context 'with an authorized user' do
it 'returns all custom attributes' do
- get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin)
+ get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to contain_exactly(
@@ -122,7 +122,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
- describe "GET /#{attributable_name}/:id/custom_attributes/:key" do
+ describe "GET /#{attributable_name}/:id/custom_attributes/:key", :aggregate_failures do
context 'with an unauthorized user' do
subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) }
@@ -131,7 +131,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
context 'with an authorized user' do
it 'returns a single custom attribute' do
- get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
+ get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' })
@@ -139,7 +139,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
- describe "PUT /#{attributable_name}/:id/custom_attributes/:key" do
+ describe "PUT /#{attributable_name}/:id/custom_attributes/:key", :aggregate_failures do
context 'with an unauthorized user' do
subject { put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user), params: { value: 'new' } }
@@ -149,7 +149,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
context 'with an authorized user' do
it 'creates a new custom attribute' do
expect do
- put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), params: { value: 'new' }
+ put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin, admin_mode: true), params: { value: 'new' }
end.to change { attributable.custom_attributes.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
@@ -159,7 +159,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
it 'updates an existing custom attribute' do
expect do
- put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), params: { value: 'new' }
+ put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin, admin_mode: true), params: { value: 'new' }
end.not_to change { attributable.custom_attributes.count }
expect(response).to have_gitlab_http_status(:ok)
@@ -169,7 +169,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
- describe "DELETE /#{attributable_name}/:id/custom_attributes/:key" do
+ describe "DELETE /#{attributable_name}/:id/custom_attributes/:key", :aggregate_failures do
context 'with an unauthorized user' do
subject { delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) }
@@ -179,7 +179,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
context 'with an authorized user' do
it 'deletes an existing custom attribute' do
expect do
- delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
+ delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin, admin_mode: true)
end.to change { attributable.custom_attributes.count }.by(-1)
expect(response).to have_gitlab_http_status(:no_content)
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index 6d29076da0f..bc7ad570441 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -165,3 +165,41 @@ RSpec.shared_examples 'Debian packages write endpoint' do |desired_behavior, suc
it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic
end
+
+RSpec.shared_examples 'Debian packages endpoint catching ObjectStorage::RemoteStoreError' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it "returns forbidden" do
+ expect(::Packages::Debian::CreatePackageFileService).to receive(:new).and_raise ObjectStorage::RemoteStoreError
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian packages index endpoint' do |success_body|
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body
+
+ context 'when no ComponentFile is found' do
+ let(:target_component_name) { component.name + FFaker::Lorem.word }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/
+ end
+end
+
+RSpec.shared_examples 'Debian packages index sha256 endpoint' do |success_body|
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body
+
+ context 'with empty checksum' do
+ let(:target_sha256) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/
+ end
+
+ context 'when ComponentFile is not found' do
+ let(:target_component_name) { component.name + FFaker::Lorem.word }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /^{"message":"404 Not Found"}$/
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
index f577e2ad323..2996c794e52 100644
--- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
@@ -123,18 +123,6 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name,
expect_snowplow_event(category: 'Notes::CreateService', action: 'execute', label: 'note', value: anything)
end
- context 'with notes_create_service_tracking feature flag disabled' do
- before do
- stub_feature_flags(notes_create_service_tracking: false)
- end
-
- it 'does not track Notes::CreateService events', :snowplow do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' }
-
- expect_no_snowplow_event(category: 'Notes::CreateService', action: 'execute')
- end
- end
-
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
@@ -243,8 +231,7 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name,
it 'returns a 404 error when note id not found' do
put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user),
- params: { body: 'Hello!' }
+ "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user), params: { body: 'Hello!' }
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
index 6c8b792bf92..930c47dac52 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -480,6 +480,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
context 'when fetching escalation status' do
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
+ let_it_be(:incident_type) { WorkItems::Type.default_by_type(:incident) }
let(:fields) do
<<~QUERY
@@ -491,7 +492,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
before do
- issue_a.update_columns(issue_type: Issue.issue_types[:incident])
+ issue_a.update_columns(issue_type: WorkItems::Type.base_types[:incident], work_item_type_id: incident_type.id)
end
it 'returns the escalation status values' do
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
index b459e479c91..53329c5caec 100644
--- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
@@ -6,7 +6,7 @@ RSpec.shared_examples 'when the snippet is not found' do
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
RSpec.shared_examples 'snippet edit usage data counters' do
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb
index 40b88ef370f..4dc0264172f 100644
--- a/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb
@@ -36,9 +36,9 @@ RSpec.shared_examples 'a subscribable resource api' do
context 'when the user is not authorized' do
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["The resource that you are attempting to access "\
- "does not exist or you don't have permission to "\
- "perform this action"]
+ errors: ["The resource that you are attempting to access "\
+ "does not exist or you don't have permission to "\
+ "perform this action"]
end
context 'when user is authorized' do
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index f5835460a77..5e9dfc826d4 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -279,11 +279,11 @@ RSpec.shared_examples 'group and project packages query' do
end
def npm_pipeline_ids
- graphql_data_npm_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] }
+ graphql_data_npm_package.dig('pipelines', 'nodes').pluck('id')
end
def composer_pipeline_ids
- graphql_data_composer_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] }
+ graphql_data_composer_package.dig('pipelines', 'nodes').pluck('id')
end
def graphql_data_npm_package
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index b4019d7c232..161f4a02b8c 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -38,7 +38,7 @@ RSpec.shared_examples 'a package with files' do
context 'with package files pending destruction' do
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
- let(:response_package_file_ids) { package_files_response.map { |pf| pf['id'] } }
+ let(:response_package_file_ids) { package_files_response.pluck('id') }
it 'does not return them' do
expect(package.reload.package_files).to include(package_file_pending_destruction)
diff --git a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
index 6b4d8cae2ce..6648c18fb70 100644
--- a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
@@ -13,13 +13,15 @@ RSpec.shared_examples 'a GraphQL query for access levels' do |access_level_kind|
let(:maintainer_access_level) { access_levels.for_role.first }
let(:maintainer_access_level_data) { access_levels_data.first }
let(:access_levels_data) do
- graphql_data_at('project',
- 'branchRules',
- 'nodes',
- 0,
- 'branchProtection',
- "#{access_level_kind.to_s.camelize(:lower)}AccessLevels",
- 'nodes')
+ graphql_data_at(
+ 'project',
+ 'branchRules',
+ 'nodes',
+ 0,
+ 'branchProtection',
+ "#{access_level_kind.to_s.camelize(:lower)}AccessLevels",
+ 'nodes'
+ )
end
let(:query) do
diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
index f2002de4b55..a2c34aa6a54 100644
--- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
RSpec.shared_examples 'web-hook API endpoints test hook' do |prefix|
- describe "POST #{prefix}/:hook_id" do
+ describe "POST #{prefix}/:hook_id", :aggregate_failures do
it 'tests the hook' do
expect(WebHookService)
.to receive(:new).with(hook, anything, String, force: false)
.and_return(instance_double(WebHookService, execute: nil))
- post api(hook_uri, user)
+ post api(hook_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:created)
end
@@ -17,7 +17,7 @@ end
RSpec.shared_examples 'web-hook API endpoints with branch-filter' do |prefix|
describe "POST #{prefix}/hooks" do
it "returns a 422 error if branch filter is not valid" do
- post api(collection_uri, user),
+ post api(collection_uri, user, admin_mode: user.admin?),
params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
@@ -58,10 +58,10 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
let(:default_values) { {} }
- describe "GET #{prefix}/hooks" do
+ describe "GET #{prefix}/hooks", :aggregate_failures do
context "authorized user" do
it "returns all hooks" do
- get api(collection_uri, user)
+ get api(collection_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_collection_schema
@@ -70,7 +70,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
context "when user is forbidden" do
it "prevents access to hooks" do
- get api(collection_uri, unauthorized_user)
+ get api(collection_uri, unauthorized_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -90,7 +90,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
it 'returns the names of the url variables' do
- get api(collection_uri, user)
+ get api(collection_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to contain_exactly(
@@ -102,10 +102,10 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
end
- describe "GET #{prefix}/hooks/:hook_id" do
+ describe "GET #{prefix}/hooks/:hook_id", :aggregate_failures do
context "authorized user" do
it "returns a project hook" do
- get api(hook_uri, user)
+ get api(hook_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_hook_schema
@@ -114,7 +114,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
it "returns a 404 error if hook id is not available" do
- get api(hook_uri(non_existing_record_id), user)
+ get api(hook_uri(non_existing_record_id), user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -125,7 +125,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
it "has the correct alert status", :aggregate_failures do
- get api(hook_uri, user)
+ get api(hook_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
@@ -135,12 +135,12 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
context 'the hook is backed-off' do
before do
- WebHook::FAILURE_THRESHOLD.times { hook.backoff! }
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.backoff! }
hook.backoff!
end
it "has the correct alert status", :aggregate_failures do
- get api(hook_uri, user)
+ get api(hook_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
@@ -156,7 +156,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
context "when user is forbidden" do
it "does not access an existing hook" do
- get api(hook_uri, unauthorized_user)
+ get api(hook_uri, unauthorized_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -171,13 +171,12 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
end
- describe "POST #{prefix}/hooks" do
+ describe "POST #{prefix}/hooks", :aggregate_failures do
let(:hook_creation_params) { hook_params }
it "adds hook", :aggregate_failures do
expect do
- post api(collection_uri, user),
- params: hook_creation_params
+ post api(collection_uri, user, admin_mode: user.admin?), params: hook_creation_params
end.to change { hooks_count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -201,8 +200,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
token = "secret token"
expect do
- post api(collection_uri, user),
- params: { url: "http://example.com", token: token }
+ post api(collection_uri, user, admin_mode: user.admin?), params: { url: "http://example.com", token: token }
end.to change { hooks_count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -216,19 +214,19 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
it "returns a 400 error if url not given" do
- post api(collection_uri, user), params: { event_names.first => true }
+ post api(collection_uri, user, admin_mode: user.admin?), params: { event_names.first => true }
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns a 400 error if no parameters are provided" do
- post api(collection_uri, user)
+ post api(collection_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'sets default values for events', :aggregate_failures do
- post api(collection_uri, user), params: { url: 'http://mep.mep' }
+ post api(collection_uri, user, admin_mode: user.admin?), params: { url: 'http://mep.mep' }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_hook_schema
@@ -239,22 +237,22 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
it "returns a 422 error if token not valid" do
- post api(collection_uri, user),
+ post api(collection_uri, user, admin_mode: user.admin?),
params: { url: "http://example.com", token: "foo\nbar" }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it "returns a 422 error if url not valid" do
- post api(collection_uri, user), params: { url: "ftp://example.com" }
+ post api(collection_uri, user, admin_mode: user.admin?), params: { url: "ftp://example.com" }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
- describe "PUT #{prefix}/hooks/:hook_id" do
+ describe "PUT #{prefix}/hooks/:hook_id", :aggregate_failures do
it "updates an existing hook" do
- put api(hook_uri, user), params: update_params
+ put api(hook_uri, user, admin_mode: user.admin?), params: update_params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_hook_schema
@@ -267,7 +265,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
it 'updates the URL variables' do
hook.update!(url_variables: { 'abc' => 'some value' })
- put api(hook_uri, user),
+ put api(hook_uri, user, admin_mode: user.admin?),
params: { url_variables: [{ key: 'def', value: 'other value' }] }
expect(response).to have_gitlab_http_status(:ok)
@@ -280,7 +278,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
it "adds the token without including it in the response" do
token = "secret token"
- put api(hook_uri, user), params: { url: "http://example.org", token: token }
+ put api(hook_uri, user, admin_mode: user.admin?), params: { url: "http://example.org", token: token }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["url"]).to eq("http://example.org")
@@ -291,68 +289,68 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
end
it "returns 404 error if hook id not found" do
- put api(hook_uri(non_existing_record_id), user), params: { url: 'http://example.org' }
+ put api(hook_uri(non_existing_record_id), user, admin_mode: user.admin?), params: { url: 'http://example.org' }
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 400 error if no parameters are provided" do
- put api(hook_uri, user)
+ put api(hook_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns a 422 error if url is not valid" do
- put api(hook_uri, user), params: { url: 'ftp://example.com' }
+ put api(hook_uri, user, admin_mode: user.admin?), params: { url: 'ftp://example.com' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it "returns a 422 error if token is not valid" do
- put api(hook_uri, user), params: { token: %w[foo bar].join("\n") }
+ put api(hook_uri, user, admin_mode: user.admin?), params: { token: %w[foo bar].join("\n") }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
- describe "DELETE /projects/:id/hooks/:hook_id" do
+ describe "DELETE /projects/:id/hooks/:hook_id", :aggregate_failures do
it "deletes hook from project" do
expect do
- delete api(hook_uri, user)
+ delete api(hook_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { hooks_count }.by(-1)
end
it "returns a 404 error when deleting non existent hook" do
- delete api(hook_uri(non_existing_record_id), user)
+ delete api(hook_uri(non_existing_record_id), user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns a 404 error if hook id not given" do
- delete api(collection_uri, user)
+ delete api(collection_uri, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns forbidden if a user attempts to delete hooks they do not own" do
- delete api(hook_uri, unauthorized_user)
+ delete api(hook_uri, unauthorized_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:forbidden)
expect(WebHook.exists?(hook.id)).to be_truthy
end
it_behaves_like '412 response' do
- let(:request) { api(hook_uri, user) }
+ let(:request) { api(hook_uri, user, admin_mode: user.admin?) }
end
end
describe "PUT #{prefix}/hooks/:hook_id/url_variables/:key", :aggregate_failures do
it 'sets the variable' do
expect do
- put api("#{hook_uri}/url_variables/abc", user),
- params: { value: 'some secret value' }
+ put api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?),
+ params: { value: 'some secret value' }
end.to change { hook.reload.url_variables }.to(eq('abc' => 'some secret value'))
expect(response).to have_gitlab_http_status(:no_content)
@@ -361,30 +359,30 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
it 'overwrites existing values' do
hook.update!(url_variables: { 'abc' => 'xyz', 'def' => 'other value' })
- put api("#{hook_uri}/url_variables/abc", user),
- params: { value: 'some secret value' }
+ put api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?),
+ params: { value: 'some secret value' }
expect(response).to have_gitlab_http_status(:no_content)
expect(hook.reload.url_variables).to eq('abc' => 'some secret value', 'def' => 'other value')
end
it "returns a 404 error when editing non existent hook" do
- put api("#{hook_uri(non_existing_record_id)}/url_variables/abc", user),
- params: { value: 'xyz' }
+ put api("#{hook_uri(non_existing_record_id)}/url_variables/abc", user, admin_mode: user.admin?),
+ params: { value: 'xyz' }
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns a 422 error when the key is illegal" do
- put api("#{hook_uri}/url_variables/abc%20def", user),
- params: { value: 'xyz' }
+ put api("#{hook_uri}/url_variables/abc%20def", user, admin_mode: user.admin?),
+ params: { value: 'xyz' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it "returns a 422 error when the value is illegal" do
- put api("#{hook_uri}/url_variables/abc", user),
- params: { value: '' }
+ put api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?),
+ params: { value: '' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -397,7 +395,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
it 'unsets the variable' do
expect do
- delete api("#{hook_uri}/url_variables/abc", user)
+ delete api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?)
end.to change { hook.reload.url_variables }.to(eq({ 'def' => 'other value' }))
expect(response).to have_gitlab_http_status(:no_content)
@@ -406,13 +404,13 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
it 'returns 404 for keys that do not exist' do
hook.update!(url_variables: { 'def' => 'other value' })
- delete api("#{hook_uri}/url_variables/abc", user)
+ delete api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns a 404 error when deleting a variable from a non existent hook" do
- delete api(hook_uri(non_existing_record_id) + "/url_variables/abc", user)
+ delete api(hook_uri(non_existing_record_id) + "/url_variables/abc", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb b/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb
new file mode 100644
index 00000000000..6799dec7b80
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ it 'is a reachable endpoint' do
+ subject
+
+ expect(response).not_to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
+ end
+
+ it 'presents as an endpoint that does not exist' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb b/spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb
new file mode 100644
index 00000000000..ddda9ca6bcc
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'Slack request verification' do
+ describe 'unauthorized request' do
+ shared_examples 'an unauthorized request' do
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ shared_examples 'a successful request that generates a tracked error' do
+ specify do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when the slack_app_signing_secret setting is not set' do
+ before do
+ stub_application_setting(slack_app_signing_secret: nil)
+ end
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'when the timestamp header has expired' do
+ before do
+ headers[::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER] = 5.minutes.ago.to_i.to_s
+ end
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'when the timestamp header is missing' do
+ before do
+ headers.delete(::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER)
+ end
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'when the signature header is missing' do
+ before do
+ headers.delete(::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER)
+ end
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'when the signature is not verified' do
+ before do
+ headers[::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER] = 'unverified_signature'
+ end
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'when type param is missing' do
+ it_behaves_like 'a successful request that generates a tracked error'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb
index 1045a92f332..e2c9874e7fc 100644
--- a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb
@@ -34,5 +34,14 @@ RSpec.shared_examples 'issuable update endpoint' do
expect(json_response['labels']).to include '&'
expect(json_response['labels']).to include '?'
end
+
+ it 'clears milestone when milestone_id=0' do
+ entity.update!(milestone: milestone)
+
+ put api(url, user), params: { milestone_id: 0 }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['milestone']).to be_nil
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb
index 41d21490343..fba0533251a 100644
--- a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb
@@ -9,6 +9,6 @@ RSpec.shared_examples 'fetches labels' do
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(expected_labels.size)
- expect(json_response.map { |r| r['name'] }).to match_array(expected_labels)
+ expect(json_response.pluck('name')).to match_array(expected_labels)
end
end
diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb
index 1ea11ba3d7c..ee7d0e86771 100644
--- a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb
@@ -52,7 +52,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition|
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
- expect(json_response.map { |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id])
+ expect(json_response.pluck('id')).to match_array([closed_milestone.id, other_milestone.id])
end
it 'does not return any milestone if none found' do
@@ -293,7 +293,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition|
expect(json_response).to be_an Array
# 2 for projects, 3 for group(which has another project with an issue)
expect(json_response.size).to be_between(2, 3)
- expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
+ expect(json_response.pluck('id')).to include(issue.id, confidential_issue.id)
end
it 'does not return confidential issues to team members with guest role' do
@@ -306,7 +306,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition|
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
- expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ expect(json_response.pluck('id')).to include(issue.id)
end
it 'does not return confidential issues to regular users' do
@@ -316,7 +316,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition|
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
- expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ expect(json_response.pluck('id')).to include(issue.id)
end
it 'returns issues ordered by label priority' do
diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
new file mode 100644
index 00000000000..2ca62698daf
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'MLflow|Not Found - Resource Does Not Exist' do
+ it "is Resource Does Not Exist", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:not_found)
+
+ expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
+ end
+end
+
+RSpec.shared_examples 'MLflow|Requires api scope' do
+ context 'when user has access but token has wrong scope' do
+ let(:access_token) { tokens[:read] }
+
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+end
+
+RSpec.shared_examples 'MLflow|Requires read_api scope' do
+ context 'when user has access but token has wrong scope' do
+ let(:access_token) { tokens[:no_access] }
+
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+end
+
+RSpec.shared_examples 'MLflow|Bad Request' do
+ it "is Bad Request" do
+ is_expected.to have_gitlab_http_status(:bad_request)
+ end
+end
+
+RSpec.shared_examples 'MLflow|shared error cases' do
+ context 'when not authenticated' do
+ let(:headers) { {} }
+
+ it "is Unauthorized" do
+ is_expected.to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user does not have access' do
+ let(:access_token) { tokens[:different_user] }
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when ff is disabled' do
+ let(:ff_value) { false }
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
+RSpec.shared_examples 'MLflow|Bad Request on missing required' do |keys|
+ keys.each do |key|
+ context "when \"#{key}\" is missing" do
+ let(:params) { default_params.tap { |p| p.delete(key) } }
+
+ it "is Bad Request" do
+ is_expected.to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index efe5ed3bcf9..b44ff952cdf 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
- describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes", :aggregate_failures do
context 'sorting' do
before do
params = { noteable: noteable, author: user }
@@ -12,9 +12,9 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
context 'without sort params' do
it 'sorts by created_at in descending order by default' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?)
- response_dates = json_response.map { |note| note['created_at'] }
+ response_dates = json_response.pluck('created_at')
expect(json_response.length).to eq(4)
expect(response_dates).to eq(response_dates.sort.reverse)
@@ -23,7 +23,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
it 'fetches notes using parent path as id paremeter' do
parent_id = CGI.escape(parent.full_path)
- get api("/#{parent_type}/#{parent_id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+ get api("/#{parent_type}/#{parent_id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -40,18 +40,18 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it 'page breaks first page correctly' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user, admin_mode: user.admin?)
- response_ids = json_response.map { |note| note['id'] }
+ response_ids = json_response.pluck('id')
expect(response_ids).to include(@note2.id)
expect(response_ids).not_to include(@first_note.id)
end
it 'page breaks second page correctly' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user, admin_mode: user.admin?)
- response_ids = json_response.map { |note| note['id'] }
+ response_ids = json_response.pluck('id')
expect(response_ids).not_to include(@note2.id)
expect(response_ids).to include(@first_note.id)
@@ -60,27 +60,27 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it 'sorts by ascending order when requested' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?sort=asc", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?sort=asc", user, admin_mode: user.admin?)
- response_dates = json_response.map { |note| note['created_at'] }
+ response_dates = json_response.pluck('created_at')
expect(json_response.length).to eq(4)
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at in descending order when requested' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at", user, admin_mode: user.admin?)
- response_dates = json_response.map { |note| note['updated_at'] }
+ response_dates = json_response.pluck('updated_at')
expect(json_response.length).to eq(4)
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at in ascending order when requested' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at&sort=asc", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at&sort=asc", user, admin_mode: user.admin?)
- response_dates = json_response.map { |note| note['updated_at'] }
+ response_dates = json_response.pluck('updated_at')
expect(json_response.length).to eq(4)
expect(response_dates).to eq(response_dates.sort)
@@ -88,7 +88,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it "returns an array of notes" do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -97,7 +97,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it "returns a 404 error when noteable id not found" do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{non_existing_record_id}/notes", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{non_existing_record_id}/notes", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -105,36 +105,36 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
it "returns 404 when not authorized" do
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user, admin_mode: private_user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id", :aggregate_failures do
it "returns a note by id" do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['body']).to eq(note.note)
end
it "returns a 404 error if note not found" do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user)
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes", :aggregate_failures do
let(:params) { { body: 'hi!' } }
subject do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params
end
it "creates a new note" do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' }
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: { body: 'hi!' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
@@ -143,7 +143,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it "returns a 400 bad request error if body not given" do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -158,7 +158,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
uri = "/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes"
expect do
- post api(uri, user), params: { body: 'hi!' }
+ post api(uri, user, admin_mode: user.admin?), params: { body: 'hi!' }
end.to change { Event.count }.by(1)
end
@@ -169,7 +169,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
context 'by an admin' do
it 'sets the creation time on the new note' do
admin = create(:admin)
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", admin), params: params
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
@@ -185,7 +185,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
let(:user) { project.first_owner }
it 'sets the creation time on the new note' do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
@@ -215,7 +215,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
when 'groups'
context 'by a group owner' do
it 'sets the creation time on the new note' do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
@@ -253,7 +253,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
context 'when the user is posting an award emoji on their own noteable' do
it 'creates a new note' do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: ':+1:' }
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: { body: ':+1:' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq(':+1:')
@@ -266,7 +266,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it 'responds with 404' do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user),
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user, admin_mode: private_user.admin?),
params: { body: 'Foo' }
expect(response).to have_gitlab_http_status(:not_found)
@@ -299,11 +299,11 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
end
- describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id", :aggregate_failures do
let(:params) { { body: 'Hello!' } }
subject do
- put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user), params: params
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user, admin_mode: user.admin?), params: params
end
context 'when only body param is present' do
@@ -329,40 +329,40 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it 'returns a 404 error when note id not found' do
- put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user),
- params: { body: 'Hello!' }
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user, admin_mode: user.admin?),
+ params: { body: 'Hello!' }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 400 bad request error if body is empty' do
put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "notes/#{note.id}", user), params: { body: '' }
+ "notes/#{note.id}", user, admin_mode: user.admin?), params: { body: '' }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
- describe "DELETE /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
+ describe "DELETE /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id", :aggregate_failures do
it 'deletes a note' do
delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "notes/#{note.id}", user)
+ "notes/#{note.id}", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:no_content)
# Check if note is really deleted
delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "notes/#{note.id}", user)
+ "notes/#{note.id}", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 error when note id not found' do
- delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user)
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like '412 response' do
- let(:request) { api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user) }
+ let(:request) { api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user, admin_mode: user.admin?) }
end
end
end
@@ -370,16 +370,16 @@ end
RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, noteable_type, id_name|
it_behaves_like 'noteable API', parent_type, noteable_type, id_name
- describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes", :aggregate_failures do
let(:params) { { body: 'hi!' } }
subject do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params
end
context 'with internal param' do
it "creates a confidential note if internal is set to true" do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(internal: true)
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params.merge(internal: true)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
@@ -391,7 +391,7 @@ RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, no
context 'with deprecated confidential param' do
it "creates a confidential note if confidential is set to true" do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(confidential: true)
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params.merge(confidential: true)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index b55639a6b82..f53532d00d7 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -507,55 +507,118 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
it_behaves_like 'returning response status', status
end
- shared_examples 'handling different package names, visibilities and user roles' do
- where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | :public | :anonymous | :accept | :ok
- :scoped_naming_convention | :public | :guest | :accept | :ok
- :scoped_naming_convention | :public | :reporter | :accept | :ok
- :scoped_no_naming_convention | :public | :anonymous | :accept | :ok
- :scoped_no_naming_convention | :public | :guest | :accept | :ok
- :scoped_no_naming_convention | :public | :reporter | :accept | :ok
- :unscoped | :public | :anonymous | :accept | :ok
- :unscoped | :public | :guest | :accept | :ok
- :unscoped | :public | :reporter | :accept | :ok
- :non_existing | :public | :anonymous | :reject | :not_found
- :non_existing | :public | :guest | :reject | :not_found
- :non_existing | :public | :reporter | :reject | :not_found
-
- :scoped_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_naming_convention | :private | :reporter | :accept | :ok
- :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :private | :reporter | :accept | :ok
- :unscoped | :private | :anonymous | :reject | :not_found
- :unscoped | :private | :guest | :reject | :forbidden
- :unscoped | :private | :reporter | :accept | :ok
- :non_existing | :private | :anonymous | :reject | :not_found
- :non_existing | :private | :guest | :reject | :forbidden
- :non_existing | :private | :reporter | :reject | :not_found
-
- :scoped_naming_convention | :internal | :anonymous | :reject | :not_found
- :scoped_naming_convention | :internal | :guest | :accept | :ok
- :scoped_naming_convention | :internal | :reporter | :accept | :ok
- :scoped_no_naming_convention | :internal | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :internal | :guest | :accept | :ok
- :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
- :unscoped | :internal | :anonymous | :reject | :not_found
- :unscoped | :internal | :guest | :accept | :ok
- :unscoped | :internal | :reporter | :accept | :ok
- :non_existing | :internal | :anonymous | :reject | :not_found
- :non_existing | :internal | :guest | :reject | :not_found
- :non_existing | :internal | :reporter | :reject | :not_found
+ shared_examples 'handling all conditions' do
+ where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | :public | nil | :accept | :ok
+ nil | :scoped_no_naming_convention | :public | nil | :accept | :ok
+ nil | :unscoped | :public | nil | :accept | :ok
+ nil | :non_existing | :public | nil | :reject | :not_found
+ nil | :scoped_naming_convention | :private | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | :private | nil | :reject | :not_found
+ nil | :unscoped | :private | nil | :reject | :not_found
+ nil | :non_existing | :private | nil | :reject | :not_found
+ nil | :scoped_naming_convention | :internal | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | :internal | nil | :reject | :not_found
+ nil | :unscoped | :internal | nil | :reject | :not_found
+ nil | :non_existing | :internal | nil | :reject | :not_found
+
+ :oauth | :scoped_naming_convention | :public | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | :public | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :public | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :public | :reporter | :accept | :ok
+ :oauth | :unscoped | :public | :guest | :accept | :ok
+ :oauth | :unscoped | :public | :reporter | :accept | :ok
+ :oauth | :non_existing | :public | :guest | :reject | :not_found
+ :oauth | :non_existing | :public | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :private | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :private | :reporter | :accept | :ok
+ :oauth | :unscoped | :private | :guest | :reject | :forbidden
+ :oauth | :unscoped | :private | :reporter | :accept | :ok
+ :oauth | :non_existing | :private | :guest | :reject | :forbidden
+ :oauth | :non_existing | :private | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | :internal | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | :internal | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :internal | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
+ :oauth | :unscoped | :internal | :guest | :accept | :ok
+ :oauth | :unscoped | :internal | :reporter | :accept | :ok
+ :oauth | :non_existing | :internal | :guest | :reject | :not_found
+ :oauth | :non_existing | :internal | :reporter | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | :public | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :public | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | :public | :guest | :accept | :ok
+ :personal_access_token | :unscoped | :public | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | :public | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | :public | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :private | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :private | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :private | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :private | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | :internal | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | :internal | :guest | :accept | :ok
+ :personal_access_token | :unscoped | :internal | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | :internal | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | :internal | :reporter | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | :public | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :public | :developer | :accept | :ok
+ :job_token | :unscoped | :public | :developer | :accept | :ok
+ :job_token | :non_existing | :public | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | :private | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :private | :developer | :accept | :ok
+ :job_token | :unscoped | :private | :developer | :accept | :ok
+ :job_token | :non_existing | :private | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | :internal | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :internal | :developer | :accept | :ok
+ :job_token | :unscoped | :internal | :developer | :accept | :ok
+ :job_token | :non_existing | :internal | :developer | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :unscoped | :public | nil | :accept | :ok
+ :deploy_token | :non_existing | :public | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :unscoped | :private | nil | :accept | :ok
+ :deploy_token | :non_existing | :private | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :unscoped | :internal | nil | :accept | :ok
+ :deploy_token | :non_existing | :internal | nil | :reject | :not_found
end
with_them do
- let(:anonymous) { user_role == :anonymous }
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.plaintext_token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
+ end
+ end
- subject { get(url, headers: anonymous ? {} : headers) }
+ subject { get(url, headers: headers) }
before do
- project.send("add_#{user_role}", user) unless anonymous
+ project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
end
@@ -571,20 +634,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
end
end
- shared_examples 'handling all conditions' do
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.plaintext_token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
-
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
- end
-
context 'with a group namespace' do
it_behaves_like 'handling all conditions'
end
@@ -599,7 +648,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
end
RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
- using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
let_it_be(:tag_name) { 'test' }
@@ -617,82 +665,10 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
it_behaves_like 'returning response status', status
end
- shared_examples 'handling different package names, visibilities and user roles' do
- where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_naming_convention | :public | :developer | :accept | :ok
- :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :public | :developer | :accept | :ok
- :unscoped | :public | :anonymous | :reject | :forbidden
- :unscoped | :public | :guest | :reject | :forbidden
- :unscoped | :public | :developer | :accept | :ok
- :non_existing | :public | :anonymous | :reject | :forbidden
- :non_existing | :public | :guest | :reject | :forbidden
- :non_existing | :public | :developer | :reject | :not_found
-
- :scoped_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_naming_convention | :private | :developer | :accept | :ok
- :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :private | :developer | :accept | :ok
- :unscoped | :private | :anonymous | :reject | :not_found
- :unscoped | :private | :guest | :reject | :forbidden
- :unscoped | :private | :developer | :accept | :ok
- :non_existing | :private | :anonymous | :reject | :not_found
- :non_existing | :private | :guest | :reject | :forbidden
- :non_existing | :private | :developer | :reject | :not_found
-
- :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_naming_convention | :internal | :developer | :accept | :ok
- :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :developer | :accept | :ok
- :unscoped | :internal | :anonymous | :reject | :forbidden
- :unscoped | :internal | :guest | :reject | :forbidden
- :unscoped | :internal | :developer | :accept | :ok
- :non_existing | :internal | :anonymous | :reject | :forbidden
- :non_existing | :internal | :guest | :reject | :forbidden
- :non_existing | :internal | :developer | :reject | :not_found
- end
-
- with_them do
- let(:anonymous) { user_role == :anonymous }
-
- subject { put(url, env: env, headers: headers) }
-
- before do
- project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: visibility.to_s)
- end
-
- example_name = "#{params[:expected_result]} create package tag request"
- status = params[:expected_status]
-
- if scope == :instance && params[:package_name_type] != :scoped_naming_convention
- example_name = 'reject create package tag request'
- status = :not_found
- end
-
- it_behaves_like example_name, status: status
- end
- end
-
shared_examples 'handling all conditions' do
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.plaintext_token) }
+ subject { put(url, env: env, headers: headers) }
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
-
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
+ it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :create, scope: scope
end
context 'with a group namespace' do
@@ -709,7 +685,6 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
end
RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
- using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
let_it_be(:package_tag) { create(:packages_tag, package: package) }
@@ -725,82 +700,10 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
it_behaves_like 'returning response status', status
end
- shared_examples 'handling different package names, visibilities and user roles' do
- where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_naming_convention | :public | :maintainer | :accept | :ok
- :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :public | :maintainer | :accept | :ok
- :unscoped | :public | :anonymous | :reject | :forbidden
- :unscoped | :public | :guest | :reject | :forbidden
- :unscoped | :public | :maintainer | :accept | :ok
- :non_existing | :public | :anonymous | :reject | :forbidden
- :non_existing | :public | :guest | :reject | :forbidden
- :non_existing | :public | :maintainer | :reject | :not_found
-
- :scoped_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_naming_convention | :private | :maintainer | :accept | :ok
- :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :private | :maintainer | :accept | :ok
- :unscoped | :private | :anonymous | :reject | :not_found
- :unscoped | :private | :guest | :reject | :forbidden
- :unscoped | :private | :maintainer | :accept | :ok
- :non_existing | :private | :anonymous | :reject | :not_found
- :non_existing | :private | :guest | :reject | :forbidden
- :non_existing | :private | :maintainer | :reject | :not_found
-
- :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_naming_convention | :internal | :maintainer | :accept | :ok
- :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :maintainer | :accept | :ok
- :unscoped | :internal | :anonymous | :reject | :forbidden
- :unscoped | :internal | :guest | :reject | :forbidden
- :unscoped | :internal | :maintainer | :accept | :ok
- :non_existing | :internal | :anonymous | :reject | :forbidden
- :non_existing | :internal | :guest | :reject | :forbidden
- :non_existing | :internal | :maintainer | :reject | :not_found
- end
-
- with_them do
- let(:anonymous) { user_role == :anonymous }
-
- subject { delete(url, headers: headers) }
-
- before do
- project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: visibility.to_s)
- end
-
- example_name = "#{params[:expected_result]} delete package tag request"
- status = params[:expected_status]
-
- if scope == :instance && params[:package_name_type] != :scoped_naming_convention
- example_name = 'reject delete package tag request'
- status = :not_found
- end
-
- it_behaves_like example_name, status: status
- end
- end
-
shared_examples 'handling all conditions' do
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.plaintext_token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
-
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
+ subject { delete(url, headers: headers) }
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
+ it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :delete, scope: scope
end
context 'with a group namespace' do
@@ -815,3 +718,134 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
end
end
end
+
+RSpec.shared_examples 'handling different package names, visibilities and user roles for tags create or delete' do |action:, scope: :project|
+ using RSpec::Parameterized::TableSyntax
+
+ role = action == :create ? :developer : :maintainer
+
+ where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | :public | nil | :reject | :unauthorized
+ nil | :scoped_no_naming_convention | :public | nil | :reject | :unauthorized
+ nil | :unscoped | :public | nil | :reject | :unauthorized
+ nil | :non_existing | :public | nil | :reject | :unauthorized
+ nil | :scoped_naming_convention | :private | nil | :reject | :unauthorized
+ nil | :scoped_no_naming_convention | :private | nil | :reject | :unauthorized
+ nil | :unscoped | :private | nil | :reject | :unauthorized
+ nil | :non_existing | :private | nil | :reject | :unauthorized
+ nil | :scoped_naming_convention | :internal | nil | :reject | :unauthorized
+ nil | :scoped_no_naming_convention | :internal | nil | :reject | :unauthorized
+ nil | :unscoped | :internal | nil | :reject | :unauthorized
+ nil | :non_existing | :internal | nil | :reject | :unauthorized
+
+ :oauth | :scoped_naming_convention | :public | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :public | role | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :public | role | :accept | :ok
+ :oauth | :unscoped | :public | :guest | :reject | :forbidden
+ :oauth | :unscoped | :public | role | :accept | :ok
+ :oauth | :non_existing | :public | :guest | :reject | :forbidden
+ :oauth | :non_existing | :public | role | :reject | :not_found
+ :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :private | role | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :private | role | :accept | :ok
+ :oauth | :unscoped | :private | :guest | :reject | :forbidden
+ :oauth | :unscoped | :private | role | :accept | :ok
+ :oauth | :non_existing | :private | :guest | :reject | :forbidden
+ :oauth | :non_existing | :private | role | :reject | :not_found
+ :oauth | :scoped_naming_convention | :internal | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :internal | role | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :internal | role | :accept | :ok
+ :oauth | :unscoped | :internal | :guest | :reject | :forbidden
+ :oauth | :unscoped | :internal | role | :accept | :ok
+ :oauth | :non_existing | :internal | :guest | :reject | :forbidden
+ :oauth | :non_existing | :internal | role | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | :public | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :public | role | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :public | role | :accept | :ok
+ :personal_access_token | :unscoped | :public | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :public | role | :accept | :ok
+ :personal_access_token | :non_existing | :public | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :public | role | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :private | role | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :private | role | :accept | :ok
+ :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :private | role | :accept | :ok
+ :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :private | role | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :internal | role | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :internal | role | :accept | :ok
+ :personal_access_token | :unscoped | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :internal | role | :accept | :ok
+ :personal_access_token | :non_existing | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :internal | role | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | :public | role | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :public | role | :accept | :ok
+ :job_token | :unscoped | :public | role | :accept | :ok
+ :job_token | :non_existing | :public | role | :reject | :not_found
+ :job_token | :scoped_naming_convention | :private | role | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :private | role | :accept | :ok
+ :job_token | :unscoped | :private | role | :accept | :ok
+ :job_token | :non_existing | :private | role | :reject | :not_found
+ :job_token | :scoped_naming_convention | :internal | role | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :internal | role | :accept | :ok
+ :job_token | :unscoped | :internal | role | :accept | :ok
+ :job_token | :non_existing | :internal | role | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :unscoped | :public | nil | :accept | :ok
+ :deploy_token | :non_existing | :public | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :unscoped | :private | nil | :accept | :ok
+ :deploy_token | :non_existing | :private | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :unscoped | :internal | nil | :accept | :ok
+ :deploy_token | :non_existing | :internal | nil | :reject | :not_found
+ end
+
+ with_them do
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.plaintext_token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
+ end
+ end
+
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: visibility.to_s)
+ end
+
+ example_name = "#{params[:expected_result]} #{action} package tag request"
+ status = params[:expected_status]
+
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = "reject #{action} package tag request"
+ # Due to #authenticate_non_get, anonymous requests on private resources
+ # are rejected with unauthorized status
+ status = params[:auth].nil? ? :unauthorized : :not_found
+ end
+
+ it_behaves_like example_name, status: status
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
index 1d79a61fbb0..7c20ea661b5 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -1,13 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'rejects package tags access' do |status:|
- before do
- package.update!(name: package_name) unless package_name == 'non-existing-package'
- end
-
- it_behaves_like 'returning response status', status
-end
-
RSpec.shared_examples 'accept package tags request' do |status:|
using RSpec::Parameterized::TableSyntax
include_context 'dependency proxy helpers context'
@@ -23,6 +15,7 @@ RSpec.shared_examples 'accept package tags request' do |status:|
end
it_behaves_like 'returning response status', status
+ it_behaves_like 'track event', :list_tags
it 'returns a valid json response' do
subject
@@ -63,6 +56,7 @@ RSpec.shared_examples 'accept create package tag request' do |user_type|
end
it_behaves_like 'returning response status', :no_content
+ it_behaves_like 'track event', :create_tag
it 'creates the package tag' do
expect { subject }.to change { Packages::Tag.count }.by(1)
@@ -145,6 +139,7 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type|
end
it_behaves_like 'returning response status', :no_content
+ it_behaves_like 'track event', :delete_tag
it 'returns a valid response' do
subject
@@ -190,3 +185,21 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type|
end
end
end
+
+RSpec.shared_examples 'track event' do |event_name|
+ let(:event_user) do
+ if auth == :deploy_token
+ deploy_token
+ elsif user_role
+ user
+ end
+ end
+
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, namespace: project.namespace, property: 'i_package_npm_user' }.tap do |context|
+ context[:user] = event_user if event_user
+ end
+ end
+
+ it_behaves_like 'a package tracking event', described_class.name, event_name.to_s
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index 17d8b9c7fab..7cafe8bb368 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -36,6 +36,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -72,6 +73,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s
with_them do
let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') }
let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -140,6 +142,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |e
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -207,6 +210,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -277,6 +281,7 @@ RSpec.shared_examples 'handling nuget search requests' do |example_names_with_st
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 98264baa61d..3168f25e4fa 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -143,37 +143,37 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa
end
RSpec.shared_examples 'a package tracking event' do |category, action, service_ping_context = true|
- before do
- stub_feature_flags(collect_package_events: true)
- end
-
let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
- event: snowplow_gitlab_standard_context[:property]).to_h]
+ [
+ Gitlab::Tracking::ServicePingContext.new(
+ data_source: :redis_hll,
+ event: snowplow_gitlab_standard_context[:property]
+ ).to_h
+ ]
end
it "creates a gitlab tracking event #{action}", :snowplow, :aggregate_failures do
- expect { subject }.to change { Packages::Event.count }.by(1)
+ subject
if service_ping_context
- expect_snowplow_event(category: category, action: action,
- label: "redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly",
- context: context, **snowplow_gitlab_standard_context)
+ expect_snowplow_event(
+ category: category,
+ action: action,
+ label: "redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly",
+ context: context,
+ **snowplow_gitlab_standard_context
+ )
else
expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context)
end
end
end
-RSpec.shared_examples 'not a package tracking event' do
- before do
- stub_feature_flags(collect_package_events: true)
- end
-
+RSpec.shared_examples 'not a package tracking event' do |category, action|
it 'does not create a gitlab tracking event', :snowplow, :aggregate_failures do
- expect { subject }.not_to change { Packages::Event.count }
+ subject
- expect_no_snowplow_event
+ expect_no_snowplow_event category: category, action: action
end
end
@@ -183,3 +183,15 @@ RSpec.shared_examples 'bumping the package last downloaded at field' do
.to change { package.reload.last_downloaded_at }.from(nil).to(instance_of(ActiveSupport::TimeWithZone))
end
end
+
+RSpec.shared_examples 'a successful package creation' do
+ it 'creates npm package with file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb b/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb
index 8dd2ef6ccc6..9847ea4e1e2 100644
--- a/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb
@@ -224,10 +224,10 @@ RSpec.shared_examples 'pipelines visibility table' do
project.project_feature.update!(project_feature_attributes)
project.add_role(ci_user, user_role) if user_role && user_role != :non_member
- get api(pipelines_api_path, api_user)
+ get api(pipelines_api_path, api_user, admin_mode: is_admin)
end
- it do
+ specify do
expect(response).to have_gitlab_http_status(response_status)
expect(api_response).to match(expected_response)
end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index 6065b1163c4..9bd430c3b4f 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -254,6 +254,13 @@ RSpec.shared_examples 'pypi simple API endpoint' do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) do
+ if user_role == :anonymous || (visibility_level == :public && !user_token)
+ snowplow_context
+ else
+ snowplow_context.merge(user: user)
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
@@ -269,7 +276,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do
let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" }
let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_pypi_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context.merge({ project: project, user: user }) }
it_behaves_like 'PyPI package versions', :developer, :success
end
diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
index 2154a76d765..3913d29e086 100644
--- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
let(:repository_storage_move_id) { storage_move.id }
def get_container_repository_storage_move
- get api(url, user)
+ get api(url, user, admin_mode: user.admin?)
end
it 'returns a container repository storage move', :aggregate_failures do
@@ -39,7 +39,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
shared_examples 'get container repository storage move list' do
def get_container_repository_storage_moves
- get api(url, user)
+ get api(url, user, admin_mode: user.admin?)
end
it 'returns container repository storage moves', :aggregate_failures do
@@ -70,7 +70,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
get_container_repository_storage_moves
- json_ids = json_response.map { |storage_move| storage_move['id'] }
+ json_ids = json_response.pluck('id')
expect(json_ids).to eq([storage_move.id, storage_move_middle.id, storage_move_oldest.id])
end
@@ -90,7 +90,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
let(:container_id) { non_existing_record_id }
it 'returns not found' do
- get api(url, user)
+ get api(url, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -108,7 +108,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
let(:repository_storage_move_id) { storage_move.id }
it 'returns not found' do
- get api(url, user)
+ get api(url, user, admin_mode: user.admin?)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -127,20 +127,20 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
end
- describe "POST /#{container_type}/:id/repository_storage_moves" do
+ describe "POST /#{container_type}/:id/repository_storage_moves", :aggregate_failures do
let(:container_id) { container.id }
let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
- post api(url, user), params: { destination_storage_name: destination_storage_name }
+ post api(url, user, admin_mode: user.admin?), params: { destination_storage_name: destination_storage_name }
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
- it 'schedules a container repository storage move', :aggregate_failures do
+ it 'schedules a container repository storage move' do
create_container_repository_storage_move
storage_move = container.repository_storage_moves.last
@@ -158,7 +158,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
it { expect { create_container_repository_storage_move }.to be_denied_for(:user) }
end
- context 'destination_storage_name is missing', :aggregate_failures do
+ context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules a container repository storage move' do
@@ -192,7 +192,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_moves
- post api(url, user), params: {
+ post api(url, user, admin_mode: user.admin?), params: {
source_storage_name: source_storage_name,
destination_storage_name: destination_storage_name
}
diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
index b5139bd8c99..2770e293683 100644
--- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
@@ -71,8 +71,7 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ
it 'returns a 404 error when note id not found' do
put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user),
- params: { body: 'Hello!' }
+ "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user), params: { body: 'Hello!' }
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
index 1b92eb56f54..56f2394c005 100644
--- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
@@ -1,12 +1,19 @@
# frozen_string_literal: true
RSpec.shared_examples 'raw snippet files' do
- let_it_be(:user_token) { create(:personal_access_token, user: snippet.author) }
let(:snippet_id) { snippet.id }
- let(:user) { snippet.author }
+ let_it_be(:user) { snippet.author }
let(:file_path) { '%2Egitattributes' }
let(:ref) { 'master' }
+ let_it_be(:user_token) do
+ if user.admin?
+ create(:personal_access_token, :admin_mode, user: user)
+ else
+ create(:personal_access_token, user: user)
+ end
+ end
+
subject { get api(api_path, personal_access_token: user_token) }
context 'with an invalid snippet ID' do
@@ -15,8 +22,10 @@ RSpec.shared_examples 'raw snippet files' do
it 'returns 404' do
subject
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Snippet Not Found')
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
end
end
@@ -185,7 +194,7 @@ RSpec.shared_examples 'snippet individual non-file updates' do
end
RSpec.shared_examples 'invalid snippet updates' do
- it 'returns 404 for invalid snippet id' do
+ it 'returns 404 for invalid snippet id', :aggregate_failures do
update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
expect(response).to have_gitlab_http_status(:not_found)
@@ -204,7 +213,7 @@ RSpec.shared_examples 'invalid snippet updates' do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'returns 400 if title is blank' do
+ it 'returns 400 if title is blank', :aggregate_failures do
update_snippet(params: { title: '' })
expect(response).to have_gitlab_http_status(:bad_request)
@@ -236,7 +245,9 @@ RSpec.shared_examples 'snippet access with different users' do
it 'returns the correct response' do
request_user = user_for(requester)
- get api(path, request_user)
+ admin_mode = requester == :admin
+
+ get api(path, request_user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(status)
end
@@ -250,8 +261,6 @@ RSpec.shared_examples 'snippet access with different users' do
other_user
when :admin
admin
- else
- nil
end
end
diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb
index 40843ccbd15..ff3947c0e73 100644
--- a/spec/support/shared_examples/requests/api/status_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb
@@ -21,6 +21,23 @@ RSpec.shared_examples '400 response' do
end
end
+RSpec.shared_examples '401 response' do
+ let(:message) { nil }
+
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 401' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+
+ if message.present?
+ expect(json_response['message']).to eq(message)
+ end
+ end
+end
+
RSpec.shared_examples '403 response' do
before do
# Fires the request
@@ -54,7 +71,7 @@ RSpec.shared_examples '412 response' do
let(:params) { nil }
let(:success_status) { 204 }
- context 'for a modified ressource' do
+ context 'for a modified resource' do
before do
delete request, params: params, headers: { 'HTTP_IF_UNMODIFIED_SINCE' => '1990-01-12T00:00:48-0600' }
end
@@ -65,7 +82,7 @@ RSpec.shared_examples '412 response' do
end
end
- context 'for an unmodified ressource' do
+ context 'for an unmodified resource' do
before do
delete request, params: params, headers: { 'HTTP_IF_UNMODIFIED_SINCE' => Time.now }
end
diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
index 86a1fd76d09..398421c7a79 100644
--- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
@@ -173,8 +173,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
it "returns the time stats for #{issuable_name}" do
- issuable.update!(spend_time: { duration: 1800, user_id: user.id },
- time_estimate: 3600)
+ issuable.update!(spend_time: { duration: 1800, user_id: user.id }, time_estimate: 3600)
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
index 642930dd982..4a7a7492398 100644
--- a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
@@ -7,40 +7,14 @@ RSpec.shared_examples 'applications controller - GET #show' do
expect(response).to render_template :show
end
-
- context 'when application is viewed after being created' do
- before do
- create_application
- stub_feature_flags(hash_oauth_secrets: false)
- end
-
- it 'sets `@created` instance variable to `true`' do
- get show_path
-
- expect(assigns[:created]).to eq(true)
- end
- end
-
- context 'when application is reviewed' do
- before do
- stub_feature_flags(hash_oauth_secrets: false)
- end
-
- it 'sets `@created` instance variable to `false`' do
- get show_path
-
- expect(assigns[:created]).to eq(false)
- end
- end
end
end
RSpec.shared_examples 'applications controller - POST #create' do
- it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do
- stub_feature_flags(hash_oauth_secrets: false)
+ it "sets `@created` instance variable to `true`" do
create_application
- expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true)
+ expect(assigns[:created]).to eq(true)
end
end
diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb
index d133c5ea641..2c08f946468 100644
--- a/spec/support/shared_examples/requests/graphql_shared_examples.rb
+++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb
@@ -58,5 +58,5 @@ end
RSpec.shared_examples 'a mutation on an unauthorized resource' do
it_behaves_like 'a mutation that returns top-level errors',
- errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
diff --git a/spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb b/spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb
new file mode 100644
index 00000000000..2221baf5b90
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires feature flag `cloudseed_aws` enabled' do
+ context 'when feature flag is disabled' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(cloudseed_aws: false)
+ end
+
+ it 'renders not found' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 3f457890f35..dafa324b3c6 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -567,8 +567,8 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
it 'does not throttle the requests' do
(1 + requests_per_period).times do
post registry_endpoint,
- params: { events: events }.to_json,
- headers: registry_headers.merge('Authorization' => secret_token)
+ params: { events: events }.to_json,
+ headers: registry_headers.merge('Authorization' => secret_token)
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
deleted file mode 100644
index f8a752a5673..00000000000
--- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'not accessible to non-admin users' do
- context 'with unauthenticated user' do
- it 'redirects to signin page' do
- subject
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'with authenticated non-admin user' do
- before do
- login_as(create(:user))
- end
-
- it 'returns status not_found' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'with authenticated admin user without admin mode' do
- before do
- login_as(create(:admin))
- end
-
- it 'redirects to enable admin mode' do
- subject
-
- expect(response).to redirect_to(new_admin_session_path)
- end
- end
-end
-
-# Requires subject and worker_class and status_api to be defined
-# let(:worker_class) { SelfMonitoringProjectCreateWorker }
-# let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
-# subject { post create_self_monitoring_project_admin_application_settings_path }
-RSpec.shared_examples 'triggers async worker, returns sidekiq job_id with response accepted' do
- before do
- allow(worker_class).to receive(:with_status).and_return(worker_class)
- end
-
- it 'returns sidekiq job_id of expected length' do
- subject
-
- job_id = json_response['job_id']
-
- aggregate_failures do
- expect(job_id).to be_present
- expect(job_id.length).to be <= Admin::ApplicationSettingsController::PARAM_JOB_ID_MAX_SIZE
- end
- end
-
- it 'triggers async worker' do
- expect(worker_class).to receive(:perform_async)
-
- subject
- end
-
- it 'returns accepted response' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:accepted)
- expect(json_response.keys).to contain_exactly('job_id', 'monitor_status')
- expect(json_response).to include(
- 'monitor_status' => status_api
- )
- end
- end
-
- it 'returns job_id' do
- fake_job_id = 'b5b28910d97563e58c2fe55f'
- allow(worker_class).to receive(:perform_async).and_return(fake_job_id)
-
- subject
-
- expect(json_response).to include('job_id' => fake_job_id)
- end
-end
-
-# Requires job_id and subject to be defined
-# let(:job_id) { 'job_id' }
-# subject do
-# get status_create_self_monitoring_project_admin_application_settings_path,
-# params: { job_id: job_id }
-# end
-RSpec.shared_examples 'handles invalid job_id' do
- context 'with invalid job_id' do
- let(:job_id) { 'a' * 51 }
-
- it 'returns bad_request if job_id too long' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq('message' => 'Parameter "job_id" cannot ' \
- "exceed length of #{Admin::ApplicationSettingsController::PARAM_JOB_ID_MAX_SIZE}")
- end
- end
- end
-end
-
-# Requires in_progress_message and subject to be defined
-# let(:in_progress_message) { 'Job to create self-monitoring project is in progress' }
-# subject do
-# get status_create_self_monitoring_project_admin_application_settings_path,
-# params: { job_id: job_id }
-# end
-RSpec.shared_examples 'sets polling header and returns accepted' do
- it 'sets polling header' do
- expect(::Gitlab::PollingInterval).to receive(:set_header)
-
- subject
- end
-
- it 'returns accepted' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:accepted)
- expect(json_response).to eq(
- 'message' => in_progress_message
- )
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/user_activity_shared_examples.rb b/spec/support/shared_examples/requests/user_activity_shared_examples.rb
index 37da1ce5c63..9c0165f7150 100644
--- a/spec/support/shared_examples/requests/user_activity_shared_examples.rb
+++ b/spec/support/shared_examples/requests/user_activity_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'updating of user activity' do |paths_to_visit|
before do
group = create(:group, name: 'group')
- project = create(:project, :public, namespace: group, name: 'project')
+ project = create(:project, :public, namespace: group, path: 'project')
create(:issue, project: project, iid: 10)
create(:merge_request, source_project: project, iid: 15)
diff --git a/spec/support/shared_examples/security_training_providers_importer.rb b/spec/support/shared_examples/security_training_providers_importer.rb
index 69d92964270..81b3d22ab23 100644
--- a/spec/support/shared_examples/security_training_providers_importer.rb
+++ b/spec/support/shared_examples/security_training_providers_importer.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'security training providers importer' do
end
it 'upserts security training providers' do
- expect { 2.times { subject } }.to change { security_training_providers.count }.from(0).to(2)
- expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior'])
+ expect { 3.times { subject } }.to change { security_training_providers.count }.from(0).to(3)
+ expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior', 'SecureFlag'])
end
end
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
index 32adf98969c..df01f9a5b0b 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
@@ -2,13 +2,15 @@
RSpec.shared_examples 'diff file base entity' do
it 'exposes essential attributes' do
- expect(subject).to include(:content_sha, :submodule, :submodule_link,
- :submodule_tree_url, :old_path_html,
- :new_path_html, :blob, :can_modify_blob,
- :file_hash, :file_path, :old_path, :new_path,
- :viewer, :diff_refs, :stored_externally,
- :external_storage, :renamed_file, :deleted_file,
- :a_mode, :b_mode, :new_file, :file_identifier_hash)
+ expect(subject).to include(
+ :content_sha, :submodule, :submodule_link,
+ :submodule_tree_url, :old_path_html,
+ :new_path_html, :blob, :can_modify_blob,
+ :file_hash, :file_path, :old_path, :new_path,
+ :viewer, :diff_refs, :stored_externally,
+ :external_storage, :renamed_file, :deleted_file,
+ :a_mode, :b_mode, :new_file, :file_identifier_hash
+ )
end
# Converted diff files from GitHub import does not contain blob file
@@ -30,13 +32,70 @@ RSpec.shared_examples 'diff file entity' do
it_behaves_like 'diff file base entity'
it 'exposes correct attributes' do
- expect(subject).to include(:added_lines, :removed_lines,
- :context_lines_path)
+ expect(subject).to include(:added_lines, :removed_lines, :context_lines_path)
end
- it 'includes viewer' do
- expect(subject[:viewer].with_indifferent_access)
+ context 'when a viewer' do
+ let(:collapsed) { false }
+ let(:added_lines) { 1 }
+ let(:removed_lines) { 0 }
+ let(:highlighted_lines) { nil }
+
+ before do
+ allow(diff_file).to receive(:diff_lines_for_serializer)
+ .and_return(highlighted_lines)
+
+ allow(diff_file).to receive(:added_lines)
+ .and_return(added_lines)
+
+ allow(diff_file).to receive(:removed_lines)
+ .and_return(removed_lines)
+
+ allow(diff_file).to receive(:collapsed?)
+ .and_return(collapsed)
+ end
+
+ it 'matches the schema' do
+ expect(subject[:viewer].with_indifferent_access)
.to match_schema('entities/diff_viewer')
+ end
+
+ context 'when it is a whitespace only change' do
+ it 'has whitespace_only true' do
+ expect(subject[:viewer][:whitespace_only])
+ .to eq(true)
+ end
+ end
+
+ context 'when the highlighted lines arent shown' do
+ before do
+ allow(diff_file).to receive(:text?)
+ .and_return(false)
+ end
+
+ it 'has whitespace_only nil' do
+ expect(subject[:viewer][:whitespace_only])
+ .to eq(nil)
+ end
+ end
+
+ context 'when it is a new file' do
+ let(:added_lines) { 0 }
+
+ it 'has whitespace_only false' do
+ expect(subject[:viewer][:whitespace_only])
+ .to eq(false)
+ end
+ end
+
+ context 'when it is a collapsed file' do
+ let(:collapsed) { true }
+
+ it 'has whitespace_only false' do
+ expect(subject[:viewer][:whitespace_only])
+ .to eq(false)
+ end
+ end
end
context 'diff files' do
diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
index b5e3a407b53..e8238480ced 100644
--- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
@@ -18,7 +18,8 @@ RSpec.shared_examples 'note entity' do
:noteable_note_url,
:report_abuse_path,
:resolvable,
- :type
+ :type,
+ :external_author
)
end
diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb
deleted file mode 100644
index c2252c83140..00000000000
--- a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'logs kubernetes errors' do
- let(:error_hash) do
- {
- service: service.class.name,
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: error_code
- }
- end
-
- it 'logs into kubernetes.log and Sentry' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- error,
- hash_including(error_hash)
- )
-
- service.execute
- end
-end
diff --git a/spec/support/shared_examples/services/clusters/create_service_shared_examples.rb b/spec/support/shared_examples/services/clusters/create_service_shared_examples.rb
new file mode 100644
index 00000000000..7cd76e45ecd
--- /dev/null
+++ b/spec/support/shared_examples/services/clusters/create_service_shared_examples.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'create cluster service success' do
+ it 'creates a cluster object' do
+ expect { subject }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(subject.name).to eq('test-cluster')
+ expect(subject.user).to eq(user)
+ expect(subject.project).to eq(project)
+ expect(subject.provider.gcp_project_id).to eq('gcp-project')
+ expect(subject.provider.zone).to eq('us-central1-a')
+ expect(subject.provider.num_nodes).to eq(1)
+ expect(subject.provider.machine_type).to eq('machine_type-a')
+ expect(subject.provider.access_token).to eq(access_token)
+ expect(subject.provider).to be_legacy_abac
+ expect(subject.platform).to be_nil
+ expect(subject.namespace_per_environment).to eq true
+ end
+end
+
+RSpec.shared_examples 'create cluster service error' do
+ it 'returns an error' do
+ expect { subject }.to change { Clusters::Cluster.count }.by(0)
+ expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+end
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index 58659775d8c..493a96b8dae 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -79,7 +79,8 @@ RSpec.shared_examples 'an accessible' do
let(:access) do
[{ 'type' => 'repository',
'name' => project.full_path,
- 'actions' => actions }]
+ 'actions' => actions,
+ 'meta' => { 'project_path' => project.full_path } }]
end
it_behaves_like 'a valid token'
@@ -244,12 +245,14 @@ RSpec.shared_examples 'a container registry auth service' do
{
'type' => 'repository',
'name' => project.full_path,
- 'actions' => ['pull']
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => project.full_path }
},
{
'type' => 'repository',
'name' => "#{project.full_path}/*",
- 'actions' => ['pull']
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => project.full_path }
}
]
end
@@ -822,16 +825,20 @@ RSpec.shared_examples 'a container registry auth service' do
[
{ 'type' => 'repository',
'name' => internal_project.full_path,
- 'actions' => ['pull'] },
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => internal_project.full_path } },
{ 'type' => 'repository',
'name' => private_project.full_path,
- 'actions' => ['pull'] },
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => private_project.full_path } },
{ 'type' => 'repository',
'name' => public_project.full_path,
- 'actions' => ['pull'] },
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => public_project.full_path } },
{ 'type' => 'repository',
'name' => public_project_private_container_registry.full_path,
- 'actions' => ['pull'] }
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => public_project_private_container_registry.full_path } }
]
end
end
@@ -845,10 +852,12 @@ RSpec.shared_examples 'a container registry auth service' do
[
{ 'type' => 'repository',
'name' => internal_project.full_path,
- 'actions' => ['pull'] },
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => internal_project.full_path } },
{ 'type' => 'repository',
'name' => public_project.full_path,
- 'actions' => ['pull'] }
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => public_project.full_path } }
]
end
end
@@ -862,7 +871,8 @@ RSpec.shared_examples 'a container registry auth service' do
[
{ 'type' => 'repository',
'name' => public_project.full_path,
- 'actions' => ['pull'] }
+ 'actions' => ['pull'],
+ 'meta' => { 'project_path' => public_project.full_path } }
]
end
end
@@ -1258,4 +1268,29 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
end
+
+ context 'with a project with a path containing special characters' do
+ let_it_be(:bad_project) { create(:project) }
+
+ before do
+ bad_project.update_attribute(:path, "#{bad_project.path}_")
+ end
+
+ describe '#access_token' do
+ let(:token) { described_class.access_token(['pull'], [bad_project.full_path]) }
+ let(:access) do
+ [{ 'type' => 'repository',
+ 'name' => bad_project.full_path,
+ 'actions' => ['pull'] }]
+ end
+
+ subject { { token: token } }
+
+ it_behaves_like 'a valid token'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/services/deploy_token_shared_examples.rb b/spec/support/shared_examples/services/deploy_token_shared_examples.rb
new file mode 100644
index 00000000000..814b6565497
--- /dev/null
+++ b/spec/support/shared_examples/services/deploy_token_shared_examples.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a deploy token creation service' do
+ let(:user) { create(:user) }
+ let(:deploy_token_params) { attributes_for(:deploy_token) }
+
+ describe '#execute' do
+ subject { described_class.new(entity, user, deploy_token_params).execute }
+
+ context 'when the deploy token is valid' do
+ it 'creates a new DeployToken' do
+ expect { subject }.to change { DeployToken.count }.by(1)
+ end
+
+ it 'creates a new ProjectDeployToken' do
+ expect { subject }.to change { deploy_token_class.count }.by(1)
+ end
+
+ it 'returns a DeployToken' do
+ expect(subject[:deploy_token]).to be_an_instance_of DeployToken
+ end
+
+ it 'sets the creator_id as the id of the current_user' do
+ expect(subject[:deploy_token].read_attribute(:creator_id)).to eq(user.id)
+ end
+ end
+
+ context 'when expires at date is not passed' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
+
+ it 'sets Forever.date' do
+ expect(subject[:deploy_token].read_attribute(:expires_at)).to eq(Forever.date)
+ end
+ end
+
+ context 'when username is empty string' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, username: '') }
+
+ it 'converts it to nil' do
+ expect(subject[:deploy_token].read_attribute(:username)).to be_nil
+ end
+ end
+
+ context 'when username is provided' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') }
+
+ it 'keeps the provided username' do
+ expect(subject[:deploy_token].read_attribute(:username)).to eq('deployer')
+ end
+ end
+
+ context 'when the deploy token is invalid' do
+ let(:deploy_token_params) do
+ attributes_for(:deploy_token, read_repository: false, read_registry: false, write_registry: false)
+ end
+
+ it 'does not create a new DeployToken' do
+ expect { subject }.not_to change { DeployToken.count }
+ end
+
+ it 'does not create a new ProjectDeployToken' do
+ expect { subject }.not_to change { deploy_token_class.count }
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'a deploy token deletion service' do
+ let(:user) { create(:user) }
+ let(:deploy_token_params) { { token_id: deploy_token.id } }
+
+ describe '#execute' do
+ subject { described_class.new(entity, user, deploy_token_params).execute }
+
+ it "destroys a token record and it's associated DeployToken" do
+ expect { subject }.to change { deploy_token_class.count }.by(-1)
+ .and change { DeployToken.count }.by(-1)
+ end
+
+ context 'with invalid token id' do
+ let(:deploy_token_params) { { token_id: 9999 } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/import_csv_service_shared_examples.rb b/spec/support/shared_examples/services/import_csv_service_shared_examples.rb
new file mode 100644
index 00000000000..1555497ae48
--- /dev/null
+++ b/spec/support/shared_examples/services/import_csv_service_shared_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples_for 'importer with email notification' do
+ it 'notifies user of import result' do
+ expect(Notify).to receive_message_chain(email_method, :deliver_later)
+
+ subject
+ end
+end
+
+RSpec.shared_examples 'correctly handles invalid files' do
+ shared_examples_for 'invalid file' do
+ it 'returns invalid file error' do
+ expect(subject[:success]).to eq(0)
+ expect(subject[:parse_error]).to eq(true)
+ end
+ end
+
+ context 'when given file with unsupported extension' do
+ let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
+
+ it_behaves_like 'invalid file'
+ end
+
+ context 'when given empty file' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') }
+
+ it_behaves_like 'invalid file'
+ end
+
+ context 'when given file without headers' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
+
+ it_behaves_like 'invalid file'
+ end
+end
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index a87e7c1f801..db2b448f567 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -12,7 +12,6 @@
# include_examples 'incident issue'
RSpec.shared_examples 'incident issue' do
it 'has incident as issue type' do
- expect(issue.issue_type).to eq('incident')
expect(issue.work_item_type.base_type).to eq('incident')
end
end
@@ -29,7 +28,6 @@ end
# include_examples 'not an incident issue'
RSpec.shared_examples 'not an incident issue' do
it 'has not incident as issue type' do
- expect(issue.issue_type).not_to eq('incident')
expect(issue.work_item_type.base_type).not_to eq('incident')
end
end
diff --git a/spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb
new file mode 100644
index 00000000000..1970301e4c9
--- /dev/null
+++ b/spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# Specifications for behavior common to all objects with executable attributes.
+# It can take a `default_params`.
+
+RSpec.shared_examples 'issuable record that supports quick actions' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:labels) { create_list(:label, 3, project: project) }
+
+ let(:base_params) { { title: 'My issuable title' } }
+ let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
+
+ before_all do
+ project.add_maintainer(user)
+ project.add_maintainer(assignee)
+ end
+
+ before do
+ issuable.reload
+ end
+
+ context 'with labels in command only' do
+ let(:example_params) do
+ {
+ description: "/label ~#{labels.first.name} ~#{labels.second.name}\n/unlabel ~#{labels.third.name}"
+ }
+ end
+
+ it 'attaches labels to issuable' do
+ expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+ end
+ end
+
+ context 'with labels in params and command' do
+ let(:example_params) do
+ {
+ label_ids: [labels.second.id],
+ description: "/label ~#{labels.first.name}\n/unlabel ~#{labels.third.name}"
+ }
+ end
+
+ it 'attaches all labels to issuable' do
+ expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+ end
+ end
+
+ context 'with assignee and milestone in command only' do
+ let(:example_params) do
+ {
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ }
+ end
+
+ it 'assigns and sets milestone to issuable' do
+ expect(issuable.assignees).to eq([assignee])
+ expect(issuable.milestone).to eq(milestone)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb
new file mode 100644
index 00000000000..5336e0f4c2f
--- /dev/null
+++ b/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'issuable import csv service' do |issuable_type|
+ let_it_be_with_refind(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { service.execute }
+
+ shared_examples_for 'an issuable importer' do
+ if issuable_type == 'issue'
+ it 'records the import attempt if resource is an issue' do
+ expect { subject }
+ .to change { Issues::CsvImport.where(project: project, user: user).count }
+ .by 1
+ end
+ end
+ end
+
+ describe '#execute' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'correctly handles invalid files' do
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
+ end
+
+ context 'with a file generated by Gitlab CSV export' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
+
+ it 'imports the CSV without errors' do
+ expect(subject[:success]).to eq(4)
+ expect(subject[:error_lines]).to eq([])
+ expect(subject[:parse_error]).to eq(false)
+ end
+
+ it 'correctly sets the issuable attributes' do
+ expect { subject }.to change { issuables.count }.by 4
+
+ expect(issuables.reload).to include(have_attributes({ title: 'Test Title', description: 'Test Description' }))
+ end
+
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
+ end
+
+ context 'with comma delimited file' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
+
+ it 'imports CSV without errors' do
+ expect(subject[:success]).to eq(3)
+ expect(subject[:error_lines]).to eq([])
+ expect(subject[:parse_error]).to eq(false)
+ end
+
+ it 'correctly sets the issuable attributes' do
+ expect { subject }.to change { issuables.count }.by 3
+
+ expect(issuables.reload).to include(have_attributes(title: 'Title with quote"', description: 'Description'))
+ end
+
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
+ end
+
+ context 'with tab delimited file with error row' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') }
+
+ it 'imports CSV with some error rows' do
+ expect(subject[:success]).to eq(2)
+ expect(subject[:error_lines]).to eq([3])
+ expect(subject[:parse_error]).to eq(false)
+ end
+
+ it 'correctly sets the issuable attributes' do
+ expect { subject }.to change { issuables.count }.by 2
+
+ expect(issuables.reload).to include(have_attributes(title: 'Hello', description: 'World'))
+ end
+
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
+ end
+
+ context 'with semicolon delimited file with CRLF' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') }
+
+ it 'imports CSV with a blank row' do
+ expect(subject[:success]).to eq(3)
+ expect(subject[:error_lines]).to eq([4])
+ expect(subject[:parse_error]).to eq(false)
+ end
+
+ it 'correctly sets the issuable attributes' do
+ expect { subject }.to change { issuables.count }.by 3
+
+ expect(issuables.reload).to include(have_attributes(title: 'Hello', description: 'World'))
+ end
+
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
new file mode 100644
index 00000000000..85a05bbe56d
--- /dev/null
+++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issuable update service' do
+ def update_issuable(opts)
+ described_class.new(project, user, opts).execute(open_issuable)
+ end
+
+ describe 'changing state' do
+ let(:hook_event) { :"#{closed_issuable.class.name.underscore.to_sym}_hooks" }
+
+ describe 'to reopened' do
+ let(:expected_payload) do
+ include(
+ changes: include(
+ state_id: { current: 1, previous: 2 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ ),
+ object_attributes: include(
+ state: 'opened',
+ action: 'reopen'
+ )
+ )
+ end
+
+ it 'executes hooks' do
+ hooks_container = described_class < Issues::BaseService ? project.project_namespace : project
+ expect(hooks_container).to receive(:execute_hooks).with(expected_payload, hook_event)
+ expect(hooks_container).to receive(:execute_integrations).with(expected_payload, hook_event)
+
+ described_class.new(
+ **described_class.constructor_container_arg(project),
+ current_user: user,
+ params: { state_event: 'reopen' }
+ ).execute(closed_issuable)
+ end
+ end
+
+ describe 'to closed' do
+ let(:expected_payload) do
+ include(
+ changes: include(
+ state_id: { current: 2, previous: 1 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ ),
+ object_attributes: include(
+ state: 'closed',
+ action: 'close'
+ )
+ )
+ end
+
+ it 'executes hooks' do
+ hooks_container = described_class < Issues::BaseService ? project.project_namespace : project
+ expect(hooks_container).to receive(:execute_hooks).with(expected_payload, hook_event)
+ expect(hooks_container).to receive(:execute_integrations).with(expected_payload, hook_event)
+
+ described_class.new(
+ **described_class.constructor_container_arg(project),
+ current_user: user,
+ params: { state_event: 'close' }
+ ).execute(open_issuable)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'keeps issuable labels sorted after update' do
+ before do
+ update_issuable(label_ids: [label_b.id])
+ end
+
+ context 'when label is changed' do
+ it 'keeps the labels sorted by title ASC' do
+ update_issuable({ add_label_ids: [label_a.id] })
+
+ expect(issuable.labels).to eq([label_a, label_b])
+ end
+ end
+end
+
+RSpec.shared_examples 'broadcasting issuable labels updates' do
+ before do
+ update_issuable(label_ids: [label_a.id])
+ end
+
+ context 'when label is added' do
+ it 'triggers the GraphQL subscription' do
+ expect(GraphqlTriggers).to receive(:issuable_labels_updated).with(issuable)
+
+ update_issuable(add_label_ids: [label_b.id])
+ end
+ end
+
+ context 'when label is removed' do
+ it 'triggers the GraphQL subscription' do
+ expect(GraphqlTriggers).to receive(:issuable_labels_updated).with(issuable)
+
+ update_issuable(remove_label_ids: [label_a.id])
+ end
+ end
+
+ context 'when label is unchanged' do
+ it 'does not trigger the GraphQL subscription' do
+ expect(GraphqlTriggers).not_to receive(:issuable_labels_updated).with(issuable)
+
+ update_issuable(label_ids: [label_a.id])
+ end
+ end
+end
+
+RSpec.shared_examples_for 'issuable update service updating last_edited_at values' do
+ context 'when updating the title of the issuable' do
+ let(:update_params) { { title: 'updated title' } }
+
+ it 'does not update last_edited values' do
+ expect { update_issuable }.to change { issuable.title }.from(issuable.title).to('updated title').and(
+ not_change(issuable, :last_edited_at)
+ ).and(
+ not_change(issuable, :last_edited_by)
+ )
+ end
+ end
+
+ context 'when updating the description of the issuable' do
+ let(:update_params) { { description: 'updated description' } }
+
+ it 'updates last_edited values' do
+ expect do
+ update_issuable
+ end.to change { issuable.description }.from(issuable.description).to('updated description').and(
+ change { issuable.last_edited_at }
+ ).and(
+ change { issuable.last_edited_by }
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
deleted file mode 100644
index ff7acc7e907..00000000000
--- a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples_for 'issuable update service updating last_edited_at values' do
- context 'when updating the title of the issuable' do
- let(:update_params) { { title: 'updated title' } }
-
- it 'does not update last_edited values' do
- expect { update_issuable }.to change { issuable.title }.from(issuable.title).to('updated title').and(
- not_change(issuable, :last_edited_at)
- ).and(
- not_change(issuable, :last_edited_by)
- )
- end
- end
-
- context 'when updating the description of the issuable' do
- let(:update_params) { { description: 'updated description' } }
-
- it 'updates last_edited values' do
- expect do
- update_issuable
- end.to change { issuable.description }.from(issuable.description).to('updated description').and(
- change { issuable.last_edited_at }
- ).and(
- change { issuable.last_edited_by }
- )
- end
- end
-end
diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
index e47ff2fcd59..0bf8bc4ff04 100644
--- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
@@ -34,7 +34,11 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'returns error' do
- is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
+ if issuable_type == :issue
+ is_expected.to eq(message: "Couldn't link #{issuable_type}. You must have at least the Reporter role in both projects.", status: :error, http_status: 403)
+ else
+ is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
+ end
end
it 'no relationship is created' do
diff --git a/spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb b/spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb
new file mode 100644
index 00000000000..2b2e90c0461
--- /dev/null
+++ b/spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'copy or reset relative position' do
+ before do
+ # ensure we have a relative position and it is known
+ old_issue.update!(relative_position: 1000)
+ end
+
+ context 'when moved to a project within same group hierarchy' do
+ it 'does not reset the relative_position' do
+ expect(subject.relative_position).to eq(1000)
+ end
+ end
+
+ context 'when moved to a project in a different group hierarchy' do
+ let_it_be(:new_project) { create(:project, group: create(:group)) }
+
+ it 'does reset the relative_position' do
+ expect(subject.relative_position).to be_nil
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb
new file mode 100644
index 00000000000..e77d73d1c72
--- /dev/null
+++ b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "migrating a deleted user's associated records to the ghost user" do |record_class, fields|
+ record_class_name = record_class.to_s.titleize.downcase
+
+ let(:project) do
+ case record_class
+ when MergeRequest
+ create(:project, :repository)
+ else
+ create(:project)
+ end
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ context "for a #{record_class_name} the user has created" do
+ let!(:record) { created_record }
+ let(:migrated_fields) { fields || [:author] }
+
+ it "does not delete the #{record_class_name}" do
+ service.execute
+
+ expect(record_class.find_by_id(record.id)).to be_present
+ end
+
+ it "blocks the user before migrating #{record_class_name}s to the 'Ghost User'" do
+ service.execute
+
+ expect(user).to be_blocked
+ end
+
+ it 'migrates all associated fields to the "Ghost user"' do
+ service.execute
+
+ migrated_record = record_class.find_by_id(record.id)
+
+ migrated_fields.each do |field|
+ expect(migrated_record.public_send(field)).to eq(User.ghost)
+ end
+ end
+
+ it 'will only migrate specific records during a hard_delete' do
+ service.execute(hard_delete: true)
+
+ migrated_record = record_class.find_by_id(record.id)
+
+ check_user = always_ghost ? User.ghost : user
+
+ migrated_fields.each do |field|
+ expect(migrated_record.public_send(field)).to eq(check_user)
+ end
+ end
+
+ describe "race conditions" do
+ context "when #{record_class_name} migration fails and is rolled back" do
+ before do
+ allow_next_instance_of(ActiveRecord::Associations::CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
+ end
+
+ it 'rolls back the user block' do
+ expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock a previously-blocked user" do
+ expect(user.starred_projects).to receive(:update_all).and_call_original
+ user.block
+
+ expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ it "blocks the user before #{record_class_name} migration begins" do
+ expect(service).to receive("migrate_#{record_class_name.parameterize(separator: '_').pluralize}".to_sym) do
+ expect(user.reload).to be_blocked
+ end
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
index a3042ac2e26..cb544f42765 100644
--- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
@@ -29,26 +29,76 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
- let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
- let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
- let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
- let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
- let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
- let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
-
- def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
+ let_it_be(:component_file_old_main_amd64) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'a') } # destroyed
+
+ let_it_be(:component_file_oldest_kept_contrib_all) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'b') } # oldest kept
+ let_it_be(:component_file_oldest_kept_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # oldest kept
+ let_it_be(:component_file_recent_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'd') } # kept, less than 1 hour ago
+
+ let_it_be(:component_file_empty_contrib_all_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z') } # oldest kept
+ let_it_be(:component_file_empty_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z') } # touched, as last empty
+ let_it_be(:component_file_recent_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'f') } # kept, less than 1 hour ago
+
+ let(:pool_prefix) do
+ prefix = "pool/#{distribution.codename}"
+ prefix += "/#{project.id}" if container_type == :group
+ prefix += "/#{package.name[0]}/#{package.name}/#{package.version}"
+ prefix
+ end
+
+ let(:expected_main_amd64_di_content) do
+ <<~MAIN_AMD64_DI_CONTENT
+ Section: misc
+ Priority: extra
+ Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb
+ Size: 409600
+ SHA256: #{package.package_files.with_debian_file_type(:udeb).first.file_sha256}
+ MAIN_AMD64_DI_CONTENT
+ end
+
+ let(:expected_main_amd64_di_sha256) { Digest::SHA256.hexdigest(expected_main_amd64_di_content) }
+ let!(:component_file_old_main_amd64_di) do # touched
+ create("debian_#{container_type}_component_file", :di_packages, component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: expected_main_amd64_di_sha256).tap do |cf|
+ cf.update! file: CarrierWaveStringFile.new(expected_main_amd64_di_content), size: expected_main_amd64_di_content.size
+ end
+ end
+
+ def check_component_file(
+ release_date, component_name, component_file_type, architecture_name, expected_content,
+ updated: true, id_of: nil
+ )
component_file = distribution
.component_files
.with_component_name(component_name)
.with_file_type(component_file_type)
.with_architecture_name(architecture_name)
+ .with_compression_type(nil)
.order_updated_asc
.last
+ if expected_content.nil?
+ expect(component_file).to be_nil
+ return
+ end
+
expect(component_file).not_to be_nil
- expect(component_file.updated_at).to eq(release_date)
- unless expected_content.nil?
+ if id_of
+ expect(component_file&.id).to eq(id_of.id)
+ else
+ # created
+ expect(component_file&.id).to be > component_file_old_main_amd64_di.id
+ end
+
+ if updated
+ expect(component_file.updated_at).to eq(release_date)
+ else
+ expect(component_file.updated_at).not_to eq(release_date)
+ end
+
+ if expected_content == ''
+ expect(component_file.size).to eq(0)
+ else
expect(expected_content).not_to include('MD5')
component_file.file.use_file do |file_path|
expect(File.read(file_path)).to eq(expected_content)
@@ -57,30 +107,23 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
end
it 'generates Debian distribution and component files', :aggregate_failures do
- current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
+ current_time = Time.utc(2020, 1, 25, 15, 17, 19)
travel_to(current_time) do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- components_count = 2
- architectures_count = 3
-
- initial_count = 6
- destroyed_count = 2
- updated_count = 1
- created_count = components_count * (architectures_count * 2 + 1) - updated_count
+ initial_count = 8
+ destroyed_count = 1
+ created_count = 4 # main_amd64 + main_sources + empty contrib_all + empty contrib_amd64
expect { subject }
.to not_change { Packages::Package.count }
.and not_change { Packages::PackageFile.count }
.and change { distribution.reload.updated_at }.to(current_time.round)
.and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
- .and change { component_file1.reload.updated_at }.to(current_time.round)
+ .and change { component_file_old_main_amd64_di.reload.updated_at }.to(current_time.round)
package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a
- pool_prefix = "pool/#{distribution.codename}"
- pool_prefix += "/#{project.id}" if container_type == :group
- pool_prefix += "/#{package.name[0]}/#{package.name}/#{package.version}"
expected_main_amd64_content = <<~EOF
Package: libsample0
Source: #{package.name}
@@ -120,17 +163,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
SHA256: #{package_files[3].file_sha256}
EOF
- expected_main_amd64_di_content = <<~EOF
- Section: misc
- Priority: extra
- Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb
- Size: 409600
- SHA256: #{package_files[4].file_sha256}
- EOF
-
expected_main_sources_content = <<~EOF
Package: #{package.name}
- Binary: sample-dev, libsample0, sample-udeb
+ Binary: sample-dev, libsample0, sample-udeb, sample-ddeb
Version: #{package.version}
Maintainer: #{package_files[1].debian_fields['Maintainer']}
Build-Depends: debhelper-compat (= 13)
@@ -139,13 +174,13 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Format: 3.0 (native)
Files:
#{package_files[1].file_md5} #{package_files[1].size} #{package_files[1].file_name}
- d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz
+ #{package_files[0].file_md5} 964 #{package_files[0].file_name}
Checksums-Sha256:
#{package_files[1].file_sha256} #{package_files[1].size} #{package_files[1].file_name}
- 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
+ #{package_files[0].file_sha256} 964 #{package_files[0].file_name}
Checksums-Sha1:
#{package_files[1].file_sha1} #{package_files[1].size} #{package_files[1].file_name}
- c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
+ #{package_files[0].file_sha1} 964 #{package_files[0].file_name}
Homepage: #{package_files[1].debian_fields['Homepage']}
Section: misc
Priority: extra
@@ -157,42 +192,38 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
check_component_file(current_time.round, 'main', :di_packages, 'all', nil)
- check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content)
+ check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content, id_of: component_file_old_main_amd64_di)
check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil)
check_component_file(current_time.round, 'main', :sources, nil, expected_main_sources_content)
- check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
- check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'all', '')
+ check_component_file(current_time.round, 'contrib', :packages, 'amd64', '')
check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil)
- check_component_file(current_time.round, 'contrib', :di_packages, 'all', nil)
- check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :di_packages, 'all', '', updated: false, id_of: component_file_empty_contrib_all_di)
+ check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', '', id_of: component_file_empty_contrib_amd64_di)
check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil)
check_component_file(current_time.round, 'contrib', :sources, nil, nil)
- main_amd64_size = expected_main_amd64_content.length
- main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
+ expected_main_amd64_size = expected_main_amd64_content.bytesize
+ expected_main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
- contrib_all_size = component_file1.size
- contrib_all_sha256 = component_file1.file_sha256
+ expected_main_amd64_di_size = expected_main_amd64_di_content.length
- main_amd64_di_size = expected_main_amd64_di_content.length
- main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content)
-
- main_sources_size = expected_main_sources_content.length
- main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content)
+ expected_main_sources_size = expected_main_sources_content.length
+ expected_main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content)
expected_release_content = <<~EOF
Codename: #{distribution.codename}
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Date: Sat, 25 Jan 2020 15:17:19 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:19 +0000
Acquire-By-Hash: yes
Architectures: all amd64 arm64
Components: contrib main
SHA256:
- #{contrib_all_sha256} #{contrib_all_size.to_s.rjust(8)} contrib/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages
@@ -201,11 +232,11 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages
- #{main_amd64_sha256} #{main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages
- #{main_amd64_di_sha256} #{main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages
+ #{expected_main_amd64_sha256} #{expected_main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages
+ #{expected_main_amd64_di_sha256} #{expected_main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages
- #{main_sources_sha256} #{main_sources_size.to_s.rjust(8)} main/source/Sources
+ #{expected_main_sources_sha256} #{expected_main_sources_size.to_s.rjust(8)} main/source/Sources
EOF
expected_release_content = "Suite: #{distribution.suite}\n#{expected_release_content}" if distribution.suite
@@ -222,7 +253,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
context 'without components and architectures' do
it 'generates minimal distribution', :aggregate_failures do
- travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
+ travel_to(Time.utc(2020, 1, 25, 15, 17, 18, 123456)) do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
expect { subject }
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index f63693dbf26..7a4d7f81e96 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -76,7 +76,7 @@ RSpec.shared_examples 'returns packages' do |container_type, user_type|
subject
expect(json_response.length).to eq(2)
- expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id)
+ expect(json_response.pluck('id')).to contain_exactly(package1.id, package2.id)
end
end
end
@@ -123,7 +123,7 @@ RSpec.shared_examples 'returns packages with subgroups' do |container_type, user
subject
expect(json_response.length).to eq(3)
- expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id)
+ expect(json_response.pluck('id')).to contain_exactly(package1.id, package2.id, package3.id)
end
end
end
@@ -138,7 +138,7 @@ RSpec.shared_examples 'package sorting' do |order_by|
it 'returns the sorted packages' do
subject
- expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id))
+ expect(json_response.pluck('id')).to eq(packages.map(&:id))
end
end
@@ -148,7 +148,7 @@ RSpec.shared_examples 'package sorting' do |order_by|
it 'returns the sorted packages' do
subject
- expect(json_response.map { |package| package['id'] }).to eq(packages.reverse.map(&:id))
+ expect(json_response.pluck('id')).to eq(packages.reverse.map(&:id))
end
end
end
@@ -225,7 +225,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
subject
expect(json_response.length).to eq(1)
- expect(json_response.map { |package| package['package_type'] }).to contain_exactly(package_type)
+ expect(json_response.pluck('package_type')).to contain_exactly(package_type)
end
end
end
@@ -253,7 +253,7 @@ RSpec.shared_examples 'with versionless packages' do
it 'does not return the package' do
subject
- expect(json_response.map { |package| package['id'] }).not_to include(versionless_package.id)
+ expect(json_response.pluck('id')).not_to include(versionless_package.id)
end
end
@@ -268,7 +268,7 @@ RSpec.shared_examples 'with versionless packages' do
it 'returns the package' do
subject
- expect(json_response.map { |package| package['id'] }).to include(versionless_package.id)
+ expect(json_response.pluck('id')).to include(versionless_package.id)
end
end
end
@@ -295,7 +295,7 @@ RSpec.shared_examples 'with status param' do
it 'does not return the package' do
subject
- expect(json_response.map { |package| package['id'] }).not_to include(hidden_package.id)
+ expect(json_response.pluck('id')).not_to include(hidden_package.id)
end
end
@@ -309,7 +309,7 @@ RSpec.shared_examples 'with status param' do
it 'returns the package' do
subject
- expect(json_response.map { |package| package['id'] }).to include(hidden_package.id)
+ expect(json_response.pluck('id')).to include(hidden_package.id)
end
end
end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index 9f940d27341..2070cac24b0 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -63,35 +63,6 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false)
expect(gitlab_shell.repository_exists?('default', old_repository_path)).to be(false)
end
-
- context ':repack_after_shard_migration feature flag disabled' do
- before do
- stub_feature_flags(repack_after_shard_migration: false)
- end
-
- it 'does not enqueue a GC run' do
- expect { subject.execute }
- .not_to change { Projects::GitGarbageCollectWorker.jobs.count }
- end
- end
-
- context ':repack_after_shard_migration feature flag enabled' do
- before do
- stub_feature_flags(repack_after_shard_migration: true)
- end
-
- it 'does not enqueue a GC run if housekeeping is disabled' do
- stub_application_setting(housekeeping_enabled: false)
-
- expect { subject.execute }
- .not_to change { Projects::GitGarbageCollectWorker.jobs.count }
- end
-
- it 'enqueues a GC run' do
- expect { subject.execute }
- .to change { Projects::GitGarbageCollectWorker.jobs.count }.by(1)
- end
- end
end
context 'when the filesystems are the same' do
diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
index 209be09c807..21dc3c2bf70 100644
--- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
@@ -114,7 +114,8 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
it 'fails with error' do
expect(project).to receive(:ci_config_for).and_return(unsupported_yaml)
- expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, '.gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.')
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, Gitlab::Utils::ErrorMessage.to_user_facing(
+ _(".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.")))
end
end
@@ -145,7 +146,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
let_it_be(:repository) { project.repository }
it 'is successful' do
- expect(repository).to receive(:root_ref_sha).and_raise(StandardError)
+ expect(repository).to receive(:commit).and_return(nil)
expect(result.status).to eq(:success)
end
end
@@ -168,7 +169,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
it 'returns an error' do
expect { result }.to raise_error { |error|
expect(error).to be_a(Gitlab::Graphql::Errors::MutationError)
- expect(error.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ expect(error.message).to eq('UF You must <a target="_blank" rel="noopener noreferrer" ' \
'href="http://localhost/help/user/project/repository/index.md' \
'#add-files-to-a-repository">add at least one file to the repository' \
'</a> before using Security features.')
diff --git a/spec/support/shared_examples/services/service_response_shared_examples.rb b/spec/support/shared_examples/services/service_response_shared_examples.rb
new file mode 100644
index 00000000000..e55f16a2994
--- /dev/null
+++ b/spec/support/shared_examples/services/service_response_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'returning an error service response' do |message: nil|
+ it 'returns an error service response' do
+ result = subject
+
+ expect(result).to be_error
+
+ expect(result.message).to eq(message) if message
+ end
+end
+
+RSpec.shared_examples 'returning a success service response' do |message: nil|
+ it 'returns a success service response' do
+ result = subject
+
+ expect(result).to be_success
+
+ expect(result.message).to eq(message) if message
+ end
+end
diff --git a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
index e72e8e79411..d3b3434b339 100644
--- a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'issue_edit snowplow tracking' do
let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION }
let(:label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL }
let(:namespace) { project.namespace }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
it_behaves_like 'Snowplow event tracking with RedisHLL context'
end
diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
deleted file mode 100644
index ac064ed4c33..00000000000
--- a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples "setting work item's milestone" do
- context "when 'milestone' param does not exist" do
- let(:params) { {} }
-
- it "does not set the work item's milestone" do
- expect { execute_callback }.to not_change(work_item, :milestone)
- end
- end
-
- context "when 'milestone' is not in the work item's project's hierarchy" do
- let(:another_group_milestone) { create(:milestone, group: create(:group)) }
- let(:params) { { milestone_id: another_group_milestone.id } }
-
- it "does not set the work item's milestone" do
- expect { execute_callback }.to not_change(work_item, :milestone)
- end
- end
-
- context 'when assigning a group milestone' do
- let(:params) { { milestone_id: group_milestone.id } }
-
- it "sets the work item's milestone" do
- expect { execute_callback }
- .to change { work_item.milestone }
- .from(nil)
- .to(group_milestone)
- end
- end
-
- context 'when assigning a project milestone' do
- let(:params) { { milestone_id: project_milestone.id } }
-
- it "sets the work item's milestone" do
- expect { execute_callback }
- .to change { work_item.milestone }
- .from(nil)
- .to(project_milestone)
- end
- end
-end
diff --git a/spec/support/shared_examples/views/pipeline_status_changes_email.rb b/spec/support/shared_examples/views/pipeline_status_changes_email.rb
index 698f11c2216..fe6cc5e03d2 100644
--- a/spec/support/shared_examples/views/pipeline_status_changes_email.rb
+++ b/spec/support/shared_examples/views/pipeline_status_changes_email.rb
@@ -8,12 +8,14 @@ RSpec.shared_examples 'pipeline status changes email' do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- user: user,
- ref: project.default_branch,
- sha: project.commit.sha,
- status: status)
+ create(
+ :ci_pipeline,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha,
+ status: status
+ )
end
before do
diff --git a/spec/support/shared_examples/work_items/export_and_import_shared_examples.rb b/spec/support/shared_examples/work_items/export_and_import_shared_examples.rb
new file mode 100644
index 00000000000..bbbfacfdf53
--- /dev/null
+++ b/spec/support/shared_examples/work_items/export_and_import_shared_examples.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'a exported file that can be imported' do
+ before do
+ origin_project.add_reporter(user)
+ target_project.add_reporter(user)
+ end
+
+ def export_work_items_for(project)
+ origin_work_items = WorkItem.where(project: origin_project)
+ export = described_class.new(origin_work_items, project)
+ export.email(user)
+ attachment = ActionMailer::Base.deliveries.last.attachments.first
+ file = Tempfile.new('temp_work_item_export.csv')
+ file.write(attachment.read)
+
+ file
+ end
+
+ def import_file_for(project, file)
+ uploader = FileUploader.new(project)
+ uploader.store!(file)
+ import_service = WorkItems::ImportCsvService.new(user, target_project, uploader)
+
+ import_service.execute
+ end
+
+ it 'imports work item with correct attributes', :aggregate_failures do
+ csv_file = export_work_items_for(origin_project)
+
+ imported_work_items = ::WorkItems::WorkItemsFinder.new(user, project: target_project).execute
+ expect { import_file_for(target_project, csv_file) }.to change { imported_work_items.count }.by 1
+ imported_work_item = imported_work_items.first
+ expect(imported_work_item.author).to eq(user)
+ expected_matching_fields.each do |field|
+ expect(imported_work_item.public_send(field)).to eq(work_item.public_send(field))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
index e224b71da91..095c32c3136 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
@@ -50,14 +50,20 @@ RSpec.shared_examples 'batched background migrations execution worker' do
end
describe '.max_running_jobs' do
- it 'returns MAX_RUNNING_MIGRATIONS' do
- expect(described_class.max_running_jobs).to eq(described_class::MAX_RUNNING_MIGRATIONS)
+ it 'returns database_max_running_batched_background_migrations application setting' do
+ stub_application_setting(database_max_running_batched_background_migrations: 3)
+
+ expect(described_class.max_running_jobs)
+ .to eq(Gitlab::CurrentSettings.database_max_running_batched_background_migrations)
end
end
describe '#max_running_jobs' do
- it 'returns MAX_RUNNING_MIGRATIONS' do
- expect(described_class.new.max_running_jobs).to eq(described_class::MAX_RUNNING_MIGRATIONS)
+ it 'returns database_max_running_batched_background_migrations application setting' do
+ stub_application_setting(database_max_running_batched_background_migrations: 3)
+
+ expect(described_class.new.max_running_jobs)
+ .to eq(Gitlab::CurrentSettings.database_max_running_batched_background_migrations)
end
end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
index 8ec955940c0..06877aee565 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
@@ -88,9 +88,9 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
- context 'when the feature flag is disabled' do
+ context 'when the tracking database is shared' do
before do
- stub_feature_flags(execute_batched_migrations_on_schedule: false)
+ skip_if_database_exists(tracking_database)
end
it 'does nothing' do
@@ -101,22 +101,17 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
- context 'when the feature flag is enabled' do
- let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
-
+ context 'when the tracking database is not shared' do
before do
- stub_feature_flags(execute_batched_migrations_on_schedule: true)
-
- allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
- .with(connection: base_model.connection)
- .and_return(nil)
+ skip_if_shared_database(tracking_database)
end
- context 'when database config is shared' do
- it 'does nothing' do
- expect(Gitlab::Database).to receive(:db_config_share_with)
- .with(base_model.connection_db_config).and_return('main')
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(execute_batched_migrations_on_schedule: false)
+ end
+ it 'does nothing' do
expect(worker).not_to receive(:active_migration)
expect(worker).not_to receive(:run_active_migration)
@@ -124,123 +119,146 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
- context 'when no active migrations exist' do
- context 'when parallel execution is disabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: false)
- end
+ context 'when the feature flag is enabled' do
+ let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
+ let(:connection) { base_model.connection }
+
+ before do
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
+ allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
+ .with(connection: connection)
+ .and_return(nil)
+ end
+
+ context 'when database config is shared' do
it 'does nothing' do
+ expect(Gitlab::Database).to receive(:db_config_share_with)
+ .with(base_model.connection_db_config).and_return('main')
+
+ expect(worker).not_to receive(:active_migration)
expect(worker).not_to receive(:run_active_migration)
worker.perform
end
end
- context 'when parallel execution is enabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: true)
- end
+ context 'when no active migrations exist' do
+ context 'when parallel execution is disabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: false)
+ end
- it 'does nothing' do
- expect(worker).not_to receive(:queue_migrations_for_execution)
+ it 'does nothing' do
+ expect(worker).not_to receive(:run_active_migration)
- worker.perform
+ worker.perform
+ end
end
- end
- end
- context 'when active migrations exist' do
- let(:job_interval) { 5.minutes }
- let(:lease_timeout) { 15.minutes }
- let(:lease_key) { described_class.name.demodulize.underscore }
- let(:migration_id) { 123 }
- let(:migration) do
- build(
- :batched_background_migration, :active,
- id: migration_id, interval: job_interval, table_name: table_name
- )
- end
+ context 'when parallel execution is enabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: true)
+ end
- let(:execution_worker_class) do
- case tracking_database
- when :main
- Database::BatchedBackgroundMigration::MainExecutionWorker
- when :ci
- Database::BatchedBackgroundMigration::CiExecutionWorker
+ it 'does nothing' do
+ expect(worker).not_to receive(:queue_migrations_for_execution)
+
+ worker.perform
+ end
end
end
- before do
- allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
- .with(connection: base_model.connection)
- .and_return(migration)
- end
+ context 'when active migrations exist' do
+ let(:job_interval) { 5.minutes }
+ let(:lease_timeout) { 15.minutes }
+ let(:lease_key) { described_class.name.demodulize.underscore }
+ let(:migration_id) { 123 }
+ let(:migration) do
+ build(
+ :batched_background_migration, :active,
+ id: migration_id, interval: job_interval, table_name: table_name
+ )
+ end
+
+ let(:execution_worker_class) do
+ case tracking_database
+ when :main
+ Database::BatchedBackgroundMigration::MainExecutionWorker
+ when :ci
+ Database::BatchedBackgroundMigration::CiExecutionWorker
+ end
+ end
- context 'when parallel execution is disabled' do
before do
- stub_feature_flags(batched_migrations_parallel_execution: false)
+ allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
+ .with(connection: connection)
+ .and_return(migration)
end
- let(:execution_worker) { instance_double(execution_worker_class) }
+ context 'when parallel execution is disabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: false)
+ end
- context 'when the calculated timeout is less than the minimum allowed' do
- let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT }
- let(:job_interval) { 2.minutes }
+ let(:execution_worker) { instance_double(execution_worker_class) }
- it 'sets the lease timeout to the minimum value' do
- expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout)
+ context 'when the calculated timeout is less than the minimum allowed' do
+ let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT }
+ let(:job_interval) { 2.minutes }
- expect(execution_worker_class).to receive(:new).and_return(execution_worker)
- expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
+ it 'sets the lease timeout to the minimum value' do
+ expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout)
- expect(worker).to receive(:run_active_migration).and_call_original
+ expect(execution_worker_class).to receive(:new).and_return(execution_worker)
+ expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
- worker.perform
- end
- end
+ expect(worker).to receive(:run_active_migration).and_call_original
- it 'always cleans up the exclusive lease' do
- lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+ worker.perform
+ end
+ end
- expect(lease).to receive(:try_obtain).and_return(true)
+ it 'always cleans up the exclusive lease' do
+ lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke')
- expect(lease).to receive(:cancel)
+ expect(lease).to receive(:try_obtain).and_return(true)
- expect { worker.perform }.to raise_error(RuntimeError, 'I broke')
- end
+ expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke')
+ expect(lease).to receive(:cancel)
- it 'delegetes the execution to ExecutionWorker' do
- base_model = Gitlab::Database.database_base_models[tracking_database]
+ expect { worker.perform }.to raise_error(RuntimeError, 'I broke')
+ end
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
- expect(execution_worker_class).to receive(:new).and_return(execution_worker)
- expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
+ it 'delegetes the execution to ExecutionWorker' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield
+ expect(execution_worker_class).to receive(:new).and_return(execution_worker)
+ expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
- worker.perform
+ worker.perform
+ end
end
- end
- context 'when parallel execution is enabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: true)
- end
+ context 'when parallel execution is enabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: true)
+ end
- it 'delegetes the execution to ExecutionWorker' do
- expect(Gitlab::Database::BackgroundMigration::BatchedMigration)
- .to receive(:active_migrations_distinct_on_table).with(
- connection: base_model.connection,
- limit: execution_worker_class.max_running_jobs
- ).and_return([migration])
+ it 'delegetes the execution to ExecutionWorker' do
+ expect(Gitlab::Database::BackgroundMigration::BatchedMigration)
+ .to receive(:active_migrations_distinct_on_table).with(
+ connection: base_model.connection,
+ limit: execution_worker_class.max_running_jobs
+ ).and_return([migration])
- expected_arguments = [
- [tracking_database.to_s, migration_id]
- ]
+ expected_arguments = [
+ [tracking_database.to_s, migration_id]
+ ]
- expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments)
+ expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments)
- worker.perform
+ worker.perform
+ end
end
end
end
@@ -248,7 +266,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
- describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do
+ describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_database?(tracking_database) do
include Gitlab::Database::DynamicModelHelpers
include Database::DatabaseHelpers
diff --git a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb
deleted file mode 100644
index e6da96e12ec..00000000000
--- a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-# This shared_example requires the following variables:
-# let(:service_class) { Gitlab::DatabaseImporters::SelfMonitoring::Project::DeleteService }
-# let(:service) { instance_double(service_class) }
-RSpec.shared_examples 'executes service' do
- before do
- allow(service_class).to receive(:new) { service }
- end
-
- it 'runs the service' do
- expect(service).to receive(:execute)
-
- subject.perform
- end
-end
-
-RSpec.shared_examples 'returns in_progress based on Sidekiq::Status' do
- it 'returns true when job is enqueued' do
- jid = described_class.with_status.perform_async
-
- expect(described_class.in_progress?(jid)).to eq(true)
- end
-
- it 'returns false when job does not exist' do
- expect(described_class.in_progress?('fake_jid')).to eq(false)
- end
-end